Home | History | Annotate | Download | only in dvr
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.dvr;
     18 
     19 import android.annotation.TargetApi;
     20 import android.content.ContentProviderOperation;
     21 import android.content.ContentResolver;
     22 import android.content.ContentUris;
     23 import android.content.Context;
     24 import android.content.OperationApplicationException;
     25 import android.media.tv.TvContract;
     26 import android.media.tv.TvInputInfo;
     27 import android.net.Uri;
     28 import android.os.AsyncTask;
     29 import android.os.Build;
     30 import android.os.Handler;
     31 import android.os.RemoteException;
     32 import android.support.annotation.MainThread;
     33 import android.support.annotation.NonNull;
     34 import android.support.annotation.Nullable;
     35 import android.support.annotation.VisibleForTesting;
     36 import android.support.annotation.WorkerThread;
     37 import android.util.Log;
     38 import android.util.Range;
     39 
     40 import com.android.tv.ApplicationSingletons;
     41 import com.android.tv.TvApplication;
     42 import com.android.tv.common.SoftPreconditions;
     43 import com.android.tv.common.feature.CommonFeatures;
     44 import com.android.tv.data.Channel;
     45 import com.android.tv.data.Program;
     46 import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
     47 import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
     48 import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener;
     49 import com.android.tv.dvr.data.RecordedProgram;
     50 import com.android.tv.dvr.data.ScheduledRecording;
     51 import com.android.tv.dvr.data.SeriesRecording;
     52 import com.android.tv.util.AsyncDbTask;
     53 import com.android.tv.util.Utils;
     54 
     55 import java.io.File;
     56 import java.util.ArrayList;
     57 import java.util.Arrays;
     58 import java.util.Collections;
     59 import java.util.HashMap;
     60 import java.util.List;
     61 import java.util.Map;
     62 import java.util.Map.Entry;
     63 
     64 /**
     65  * DVR manager class to add and remove recordings. UI can modify recording list through this class,
     66  * instead of modifying them directly through {@link DvrDataManager}.
     67  */
     68 @MainThread
     69 @TargetApi(Build.VERSION_CODES.N)
     70 public class DvrManager {
     71     private static final String TAG = "DvrManager";
     72     private static final boolean DEBUG = false;
     73 
     74     private final WritableDvrDataManager mDataManager;
     75     private final DvrScheduleManager mScheduleManager;
     76     // @GuardedBy("mListener")
     77     private final Map<Listener, Handler> mListener = new HashMap<>();
     78     private final Context mAppContext;
     79 
     80     public DvrManager(Context context) {
     81         SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
     82         mAppContext = context.getApplicationContext();
     83         ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
     84         mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager();
     85         mScheduleManager = appSingletons.getDvrScheduleManager();
     86         if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) {
     87             createSeriesRecordingsForRecordedProgramsIfNeeded(mDataManager.getRecordedPrograms());
     88         } else {
     89             // No need to handle DVR schedule load finished because schedule manager is initialized
     90             // after the all the schedules are loaded.
     91             if (!mDataManager.isRecordedProgramLoadFinished()) {
     92                 mDataManager.addRecordedProgramLoadFinishedListener(
     93                         new OnRecordedProgramLoadFinishedListener() {
     94                             @Override
     95                             public void onRecordedProgramLoadFinished() {
     96                                 mDataManager.removeRecordedProgramLoadFinishedListener(this);
     97                                 if (mDataManager.isInitialized()
     98                                         && mScheduleManager.isInitialized()) {
     99                                     createSeriesRecordingsForRecordedProgramsIfNeeded(
    100                                             mDataManager.getRecordedPrograms());
    101                                 }
    102                             }
    103                         });
    104             }
    105             if (!mScheduleManager.isInitialized()) {
    106                 mScheduleManager.addOnInitializeListener(new OnInitializeListener() {
    107                     @Override
    108                     public void onInitialize() {
    109                         mScheduleManager.removeOnInitializeListener(this);
    110                         if (mDataManager.isInitialized() && mScheduleManager.isInitialized()) {
    111                             createSeriesRecordingsForRecordedProgramsIfNeeded(
    112                                     mDataManager.getRecordedPrograms());
    113                         }
    114                     }
    115                 });
    116             }
    117         }
    118         mDataManager.addRecordedProgramListener(new RecordedProgramListener() {
    119             @Override
    120             public void onRecordedProgramsAdded(RecordedProgram... recordedPrograms) {
    121                 if (!mDataManager.isInitialized() || !mScheduleManager.isInitialized()) {
    122                     return;
    123                 }
    124                 for (RecordedProgram recordedProgram : recordedPrograms) {
    125                     createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram);
    126                 }
    127             }
    128 
    129             @Override
    130             public void onRecordedProgramsChanged(RecordedProgram... recordedPrograms) { }
    131 
    132             @Override
    133             public void onRecordedProgramsRemoved(RecordedProgram... recordedPrograms) {
    134                 // Removing series recording is handled in the SeriesRecordingDetailsFragment.
    135             }
    136         });
    137     }
    138 
    139     private void createSeriesRecordingsForRecordedProgramsIfNeeded(
    140             List<RecordedProgram> recordedPrograms) {
    141         for (RecordedProgram recordedProgram : recordedPrograms) {
    142             createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram);
    143         }
    144     }
    145 
    146     private void createSeriesRecordingForRecordedProgramIfNeeded(RecordedProgram recordedProgram) {
    147         if (recordedProgram.isEpisodic()) {
    148             SeriesRecording seriesRecording =
    149                     mDataManager.getSeriesRecording(recordedProgram.getSeriesId());
    150             if (seriesRecording == null) {
    151                 addSeriesRecording(recordedProgram);
    152             }
    153         }
    154     }
    155 
    156     /**
    157      * Schedules a recording for {@code program}.
    158      */
    159     public ScheduledRecording addSchedule(Program program) {
    160         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    161             return null;
    162         }
    163         SeriesRecording seriesRecording = getSeriesRecording(program);
    164         return addSchedule(program, seriesRecording == null
    165                 ? mScheduleManager.suggestNewPriority()
    166                 : seriesRecording.getPriority());
    167     }
    168 
    169     /**
    170      * Schedules a recording for {@code program} with the highest priority so that the schedule
    171      * can be recorded.
    172      */
    173     public ScheduledRecording addScheduleWithHighestPriority(Program program) {
    174         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    175             return null;
    176         }
    177         SeriesRecording seriesRecording = getSeriesRecording(program);
    178         return addSchedule(program, seriesRecording == null
    179                 ? mScheduleManager.suggestNewPriority()
    180                 : mScheduleManager.suggestHighestPriority(seriesRecording.getInputId(),
    181                         new Range(program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis()),
    182                         seriesRecording.getPriority()));
    183     }
    184 
    185     private ScheduledRecording addSchedule(Program program, long priority) {
    186         TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, program);
    187         if (input == null) {
    188             Log.e(TAG, "Can't find input for program: " + program);
    189             return null;
    190         }
    191         ScheduledRecording schedule;
    192         SeriesRecording seriesRecording = getSeriesRecording(program);
    193         schedule = createScheduledRecordingBuilder(input.getId(), program)
    194                 .setPriority(priority)
    195                 .setSeriesRecordingId(seriesRecording == null ? SeriesRecording.ID_NOT_SET
    196                         : seriesRecording.getId())
    197                 .build();
    198         mDataManager.addScheduledRecording(schedule);
    199         return schedule;
    200     }
    201 
    202     /**
    203      * Adds a recording schedule with a time range.
    204      */
    205     public void addSchedule(Channel channel, long startTime, long endTime) {
    206         Log.i(TAG, "Adding scheduled recording of channel " + channel + " starting at " +
    207                 Utils.toTimeString(startTime) + " and ending at " + Utils.toTimeString(endTime));
    208         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    209             return;
    210         }
    211         TvInputInfo input = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId());
    212         if (input == null) {
    213             Log.e(TAG, "Can't find input for channel: " + channel);
    214             return;
    215         }
    216         addScheduleInternal(input.getId(), channel.getId(), startTime, endTime);
    217     }
    218 
    219     /**
    220      * Adds the schedule.
    221      */
    222     public void addSchedule(ScheduledRecording schedule) {
    223         if (mDataManager.isDvrScheduleLoadFinished()) {
    224             mDataManager.addScheduledRecording(schedule);
    225         }
    226     }
    227 
    228     private void addScheduleInternal(String inputId, long channelId, long startTime, long endTime) {
    229         mDataManager.addScheduledRecording(ScheduledRecording
    230                 .builder(inputId, channelId, startTime, endTime)
    231                 .setPriority(mScheduleManager.suggestNewPriority())
    232                 .build());
    233     }
    234 
    235     /**
    236      * Adds a new series recording and schedules for the programs with the initial state.
    237      */
    238     public SeriesRecording addSeriesRecording(Program selectedProgram,
    239             List<Program> programsToSchedule, @SeriesRecording.SeriesState int initialState) {
    240         Log.i(TAG, "Adding series recording for program " + selectedProgram + ", and schedules: "
    241                 + programsToSchedule);
    242         if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
    243             return null;
    244         }
    245         TvInputInfo input = Utils.getTvInputInfoForProgram(mAppContext, selectedProgram);
    246         if (input == null) {
    247             Log.e(TAG, "Can't find input for program: " + selectedProgram);
    248             return null;
    249         }
    250         SeriesRecording seriesRecording = SeriesRecording.builder(input.getId(), selectedProgram)
    251                 .setPriority(mScheduleManager.suggestNewSeriesPriority())
    252                 .setState(initialState)
    253                 .build();
    254         mDataManager.addSeriesRecording(seriesRecording);
    255         // The schedules for the recorded programs should be added not to create the schedule the
    256         // duplicate episodes.
    257         addRecordedProgramToSeriesRecording(seriesRecording);
    258         addScheduleToSeriesRecording(seriesRecording, programsToSchedule);
    259         return seriesRecording;
    260     }
    261 
    262     private void addSeriesRecording(RecordedProgram recordedProgram) {
    263         SeriesRecording seriesRecording =
    264                 SeriesRecording.builder(recordedProgram.getInputId(), recordedProgram)
    265                         .setPriority(mScheduleManager.suggestNewSeriesPriority())
    266                         .setState(SeriesRecording.STATE_SERIES_STOPPED)
    267                         .build();
    268         mDataManager.addSeriesRecording(seriesRecording);
    269         // The schedules for the recorded programs should be added not to create the schedule the
    270         // duplicate episodes.
    271         addRecordedProgramToSeriesRecording(seriesRecording);
    272     }
    273 
    274     private void addRecordedProgramToSeriesRecording(SeriesRecording series) {
    275         List<ScheduledRecording> toAdd = new ArrayList<>();
    276         for (RecordedProgram recordedProgram : mDataManager.getRecordedPrograms()) {
    277             if (series.getSeriesId().equals(recordedProgram.getSeriesId())
    278                     && !recordedProgram.isClipped()) {
    279                 // Duplicate schedules can exist, but they will be deleted in a few days. And it's
    280                 // also guaranteed that the schedules don't belong to any series recordings because
    281                 // there are no more than one series recordings which have the same program title.
    282                 toAdd.add(ScheduledRecording.builder(recordedProgram)
    283                         .setPriority(series.getPriority())
    284                         .setSeriesRecordingId(series.getId()).build());
    285             }
    286         }
    287         if (!toAdd.isEmpty()) {
    288             mDataManager.addScheduledRecording(ScheduledRecording.toArray(toAdd));
    289         }
    290     }
    291 
    292     /**
    293      * Adds {@link ScheduledRecording}s for the series recording.
    294      * <p>
    295      * This method doesn't add the series recording.
    296      */
    297     public void addScheduleToSeriesRecording(SeriesRecording series,
    298             List<Program> programsToSchedule) {
    299         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    300             return;
    301         }
    302         TvInputInfo input = Utils.getTvInputInfoForInputId(mAppContext, series.getInputId());
    303         if (input == null) {
    304             Log.e(TAG, "Can't find input with ID: " + series.getInputId());
    305             return;
    306         }
    307         List<ScheduledRecording> toAdd = new ArrayList<>();
    308         List<ScheduledRecording> toUpdate = new ArrayList<>();
    309         for (Program program : programsToSchedule) {
    310             ScheduledRecording scheduleWithSameProgram =
    311                     mDataManager.getScheduledRecordingForProgramId(program.getId());
    312             if (scheduleWithSameProgram != null) {
    313                 if (scheduleWithSameProgram.isNotStarted()) {
    314                     ScheduledRecording r = ScheduledRecording.buildFrom(scheduleWithSameProgram)
    315                             .setSeriesRecordingId(series.getId())
    316                             .build();
    317                     if (!r.equals(scheduleWithSameProgram)) {
    318                         toUpdate.add(r);
    319                     }
    320                 }
    321             } else {
    322                 toAdd.add(createScheduledRecordingBuilder(input.getId(), program)
    323                         .setPriority(series.getPriority())
    324                         .setSeriesRecordingId(series.getId())
    325                         .build());
    326             }
    327         }
    328         if (!toAdd.isEmpty()) {
    329             mDataManager.addScheduledRecording(ScheduledRecording.toArray(toAdd));
    330         }
    331         if (!toUpdate.isEmpty()) {
    332             mDataManager.updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
    333         }
    334     }
    335 
    336     /**
    337      * Updates the series recording.
    338      */
    339     public void updateSeriesRecording(SeriesRecording series) {
    340         if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    341             SeriesRecording previousSeries = mDataManager.getSeriesRecording(series.getId());
    342             if (previousSeries != null) {
    343                 // If the channel option of series changed, remove the existing schedules. The new
    344                 // schedules will be added by SeriesRecordingScheduler or by SeriesSettingsFragment.
    345                 if (previousSeries.getChannelOption() != series.getChannelOption()
    346                         || (previousSeries.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
    347                         && previousSeries.getChannelId() != series.getChannelId())) {
    348                     List<ScheduledRecording> schedules =
    349                             mDataManager.getScheduledRecordings(series.getId());
    350                     List<ScheduledRecording> schedulesToRemove = new ArrayList<>();
    351                     for (ScheduledRecording schedule : schedules) {
    352                         if (schedule.isNotStarted()) {
    353                             schedulesToRemove.add(schedule);
    354                         } else if (schedule.isInProgress()
    355                                 && series.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
    356                                 && schedule.getChannelId() != series.getChannelId()) {
    357                             stopRecording(schedule);
    358                         }
    359                     }
    360                     List<ScheduledRecording> deletedSchedules =
    361                             new ArrayList<>(mDataManager.getDeletedSchedules());
    362                     for (ScheduledRecording deletedSchedule : deletedSchedules) {
    363                         if (deletedSchedule.getSeriesRecordingId() == series.getId()
    364                                 && deletedSchedule.getEndTimeMs() > System.currentTimeMillis()) {
    365                             schedulesToRemove.add(deletedSchedule);
    366                         }
    367                     }
    368                     mDataManager.removeScheduledRecording(true,
    369                             ScheduledRecording.toArray(schedulesToRemove));
    370                 }
    371             }
    372             mDataManager.updateSeriesRecording(series);
    373             if (previousSeries == null
    374                     || previousSeries.getPriority() != series.getPriority()) {
    375                 long priority = series.getPriority();
    376                 List<ScheduledRecording> schedulesToUpdate = new ArrayList<>();
    377                 for (ScheduledRecording schedule
    378                         : mDataManager.getScheduledRecordings(series.getId())) {
    379                     if (schedule.isNotStarted() || schedule.isInProgress()) {
    380                         schedulesToUpdate.add(ScheduledRecording.buildFrom(schedule)
    381                                 .setPriority(priority).build());
    382                     }
    383                 }
    384                 if (!schedulesToUpdate.isEmpty()) {
    385                     mDataManager.updateScheduledRecording(
    386                             ScheduledRecording.toArray(schedulesToUpdate));
    387                 }
    388             }
    389         }
    390     }
    391 
    392     /**
    393      * Removes the series recording and all the corresponding schedules which are not started yet.
    394      */
    395     public void removeSeriesRecording(long seriesRecordingId) {
    396         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    397             return;
    398         }
    399         SeriesRecording series = mDataManager.getSeriesRecording(seriesRecordingId);
    400         if (series == null) {
    401             return;
    402         }
    403         for (ScheduledRecording schedule : mDataManager.getAllScheduledRecordings()) {
    404             if (schedule.getSeriesRecordingId() == seriesRecordingId) {
    405                 if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
    406                     stopRecording(schedule);
    407                     break;
    408                 }
    409             }
    410         }
    411         mDataManager.removeSeriesRecording(series);
    412     }
    413 
    414     /**
    415      * Stops the currently recorded program
    416      */
    417     public void stopRecording(final ScheduledRecording recording) {
    418         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    419             return;
    420         }
    421         synchronized (mListener) {
    422             for (final Entry<Listener, Handler> entry : mListener.entrySet()) {
    423                 entry.getValue().post(new Runnable() {
    424                     @Override
    425                     public void run() {
    426                         entry.getKey().onStopRecordingRequested(recording);
    427                     }
    428                 });
    429             }
    430         }
    431     }
    432 
    433     /**
    434      * Removes scheduled recordings or an existing recordings.
    435      */
    436     public void removeScheduledRecording(ScheduledRecording... schedules) {
    437         Log.i(TAG, "Removing " + Arrays.asList(schedules));
    438         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    439             return;
    440         }
    441         for (ScheduledRecording r : schedules) {
    442             if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
    443                 stopRecording(r);
    444             } else {
    445                 mDataManager.removeScheduledRecording(r);
    446             }
    447         }
    448     }
    449 
    450     /**
    451      * Removes scheduled recordings without changing to the DELETED state.
    452      */
    453     public void forceRemoveScheduledRecording(ScheduledRecording... schedules) {
    454         Log.i(TAG, "Force removing " + Arrays.asList(schedules));
    455         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    456             return;
    457         }
    458         for (ScheduledRecording r : schedules) {
    459             if (r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
    460                 stopRecording(r);
    461             } else {
    462                 mDataManager.removeScheduledRecording(true, r);
    463             }
    464         }
    465     }
    466 
    467     /**
    468      * Removes the recorded program. It deletes the file if possible.
    469      */
    470     public void removeRecordedProgram(Uri recordedProgramUri) {
    471         if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
    472             return;
    473         }
    474         removeRecordedProgram(ContentUris.parseId(recordedProgramUri));
    475     }
    476 
    477     /**
    478      * Removes the recorded program. It deletes the file if possible.
    479      */
    480     public void removeRecordedProgram(long recordedProgramId) {
    481         if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
    482             return;
    483         }
    484         RecordedProgram recordedProgram = mDataManager.getRecordedProgram(recordedProgramId);
    485         if (recordedProgram != null) {
    486             removeRecordedProgram(recordedProgram);
    487         }
    488     }
    489 
    490     /**
    491      * Removes the recorded program. It deletes the file if possible.
    492      */
    493     public void removeRecordedProgram(final RecordedProgram recordedProgram) {
    494         if (!SoftPreconditions.checkState(mDataManager.isInitialized())) {
    495             return;
    496         }
    497         new AsyncDbTask<Void, Void, Integer>() {
    498             @Override
    499             protected Integer doInBackground(Void... params) {
    500                 ContentResolver resolver = mAppContext.getContentResolver();
    501                 return resolver.delete(recordedProgram.getUri(), null, null);
    502             }
    503 
    504             @Override
    505             protected void onPostExecute(Integer deletedCounts) {
    506                 if (deletedCounts > 0) {
    507                     new AsyncTask<Void, Void, Void>() {
    508                         @Override
    509                         protected Void doInBackground(Void... params) {
    510                             removeRecordedData(recordedProgram.getDataUri());
    511                             return null;
    512                         }
    513                     }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    514                 }
    515             }
    516         }.executeOnDbThread();
    517     }
    518 
    519     public void removeRecordedPrograms(List<Long> recordedProgramIds) {
    520         final ArrayList<ContentProviderOperation> dbOperations = new ArrayList<>();
    521         final List<Uri> dataUris = new ArrayList<>();
    522         for (Long rId : recordedProgramIds) {
    523             RecordedProgram r = mDataManager.getRecordedProgram(rId);
    524             if (r != null) {
    525                 dataUris.add(r.getDataUri());
    526                 dbOperations.add(ContentProviderOperation.newDelete(r.getUri()).build());
    527             }
    528         }
    529         new AsyncDbTask<Void, Void, Boolean>() {
    530             @Override
    531             protected Boolean doInBackground(Void... params) {
    532                 ContentResolver resolver = mAppContext.getContentResolver();
    533                 try {
    534                     resolver.applyBatch(TvContract.AUTHORITY, dbOperations);
    535                 } catch (RemoteException | OperationApplicationException e) {
    536                     Log.w(TAG, "Remove recorded programs from DB failed.", e);
    537                     return false;
    538                 }
    539                 return true;
    540             }
    541 
    542             @Override
    543             protected void onPostExecute(Boolean success) {
    544                 if (success) {
    545                     new AsyncTask<Void, Void, Void>() {
    546                         @Override
    547                         protected Void doInBackground(Void... params) {
    548                             for (Uri dataUri : dataUris) {
    549                                 removeRecordedData(dataUri);
    550                             }
    551                             return null;
    552                         }
    553                     }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    554                 }
    555             }
    556         }.executeOnDbThread();
    557     }
    558 
    559     /**
    560      * Updates the scheduled recording.
    561      */
    562     public void updateScheduledRecording(ScheduledRecording recording) {
    563         if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    564             mDataManager.updateScheduledRecording(recording);
    565         }
    566     }
    567 
    568     /**
    569      * Returns priority ordered list of all scheduled recordings that will not be recorded if
    570      * this program is.
    571      *
    572      * @see DvrScheduleManager#getConflictingSchedules(Program)
    573      */
    574     public List<ScheduledRecording> getConflictingSchedules(Program program) {
    575         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    576             return Collections.emptyList();
    577         }
    578         return mScheduleManager.getConflictingSchedules(program);
    579     }
    580 
    581     /**
    582      * Returns priority ordered list of all scheduled recordings that will not be recorded if
    583      * this channel is.
    584      *
    585      * @see DvrScheduleManager#getConflictingSchedules(long, long, long)
    586      */
    587     public List<ScheduledRecording> getConflictingSchedules(long channelId, long startTimeMs,
    588             long endTimeMs) {
    589         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    590             return Collections.emptyList();
    591         }
    592         return mScheduleManager.getConflictingSchedules(channelId, startTimeMs, endTimeMs);
    593     }
    594 
    595     /**
    596      * Checks if the schedule is conflicting.
    597      *
    598      * <p>Note that the {@code schedule} should be the existing one. If not, this returns
    599      * {@code false}.
    600      */
    601     public boolean isConflicting(ScheduledRecording schedule) {
    602         return schedule != null
    603                 && SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())
    604                 && mScheduleManager.isConflicting(schedule);
    605     }
    606 
    607     /**
    608      * Returns priority ordered list of all scheduled recording that will not be recorded if
    609      * this channel is tuned to.
    610      *
    611      * @see DvrScheduleManager#getConflictingSchedulesForTune
    612      */
    613     public List<ScheduledRecording> getConflictingSchedulesForTune(long channelId) {
    614         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    615             return Collections.emptyList();
    616         }
    617         return mScheduleManager.getConflictingSchedulesForTune(channelId);
    618     }
    619 
    620     /**
    621      * Sets the highest priority to the schedule.
    622      */
    623     public void setHighestPriority(ScheduledRecording schedule) {
    624         if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    625             long newPriority = mScheduleManager.suggestHighestPriority(schedule);
    626             if (newPriority != schedule.getPriority()) {
    627                 mDataManager.updateScheduledRecording(ScheduledRecording.buildFrom(schedule)
    628                         .setPriority(newPriority).build());
    629             }
    630         }
    631     }
    632 
    633     /**
    634      * Suggests the higher priority than the schedules which overlap with {@code schedule}.
    635      */
    636     public long suggestHighestPriority(ScheduledRecording schedule) {
    637         if (SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    638             return mScheduleManager.suggestHighestPriority(schedule);
    639         }
    640         return DvrScheduleManager.DEFAULT_PRIORITY;
    641     }
    642 
    643     /**
    644      * Returns {@code true} if the channel can be recorded.
    645      * <p>
    646      * Note that this method doesn't check the conflict of the schedule or available tuners.
    647      * This can be called from the UI before the schedules are loaded.
    648      */
    649     public boolean isChannelRecordable(Channel channel) {
    650         if (!mDataManager.isDvrScheduleLoadFinished() || channel == null) {
    651             return false;
    652         }
    653         if (channel.isRecordingProhibited()) {
    654             return false;
    655         }
    656         TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId());
    657         if (info == null) {
    658             Log.w(TAG, "Could not find TvInputInfo for " + channel);
    659             return false;
    660         }
    661         if (!info.canRecord()) {
    662             return false;
    663         }
    664         Program program = TvApplication.getSingletons(mAppContext).getProgramDataManager()
    665                 .getCurrentProgram(channel.getId());
    666         return program == null || !program.isRecordingProhibited();
    667     }
    668 
    669     /**
    670      * Returns {@code true} if the program can be recorded.
    671      * <p>
    672      * Note that this method doesn't check the conflict of the schedule or available tuners.
    673      * This can be called from the UI before the schedules are loaded.
    674      */
    675     public boolean isProgramRecordable(Program program) {
    676         if (!mDataManager.isInitialized()) {
    677             return false;
    678         }
    679         Channel channel = TvApplication.getSingletons(mAppContext).getChannelDataManager()
    680                 .getChannel(program.getChannelId());
    681         if (channel == null || channel.isRecordingProhibited()) {
    682             return false;
    683         }
    684         TvInputInfo info = Utils.getTvInputInfoForChannelId(mAppContext, channel.getId());
    685         if (info == null) {
    686             Log.w(TAG, "Could not find TvInputInfo for " + program);
    687             return false;
    688         }
    689         return info.canRecord() && !program.isRecordingProhibited();
    690     }
    691 
    692     /**
    693      * Returns the current recording for the channel.
    694      * <p>
    695      * This can be called from the UI before the schedules are loaded.
    696      */
    697     public ScheduledRecording getCurrentRecording(long channelId) {
    698         if (!mDataManager.isDvrScheduleLoadFinished()) {
    699             return null;
    700         }
    701         for (ScheduledRecording recording : mDataManager.getStartedRecordings()) {
    702             if (recording.getChannelId() == channelId) {
    703                 return recording;
    704             }
    705         }
    706         return null;
    707     }
    708 
    709     /**
    710      * Returns schedules which is available (i.e., isNotStarted or isInProgress) and belongs to
    711      * the series recording {@code seriesRecordingId}.
    712      */
    713     public List<ScheduledRecording> getAvailableScheduledRecording(long seriesRecordingId) {
    714         if (!mDataManager.isDvrScheduleLoadFinished()) {
    715             return Collections.emptyList();
    716         }
    717         List<ScheduledRecording> schedules = new ArrayList<>();
    718         for (ScheduledRecording schedule : mDataManager.getScheduledRecordings(seriesRecordingId)) {
    719             if (schedule.isInProgress() || schedule.isNotStarted()) {
    720                 schedules.add(schedule);
    721             }
    722         }
    723         return schedules;
    724     }
    725 
    726     /**
    727      * Returns the series recording related to the program.
    728      */
    729     @Nullable
    730     public SeriesRecording getSeriesRecording(Program program) {
    731         if (!SoftPreconditions.checkState(mDataManager.isDvrScheduleLoadFinished())) {
    732             return null;
    733         }
    734         return mDataManager.getSeriesRecording(program.getSeriesId());
    735     }
    736 
    737     /**
    738      * Returns if there are valid items. Valid item contains {@link RecordedProgram},
    739      * available {@link ScheduledRecording} and {@link SeriesRecording}.
    740      */
    741     public boolean hasValidItems() {
    742         return !(mDataManager.getRecordedPrograms().isEmpty()
    743                 && mDataManager.getStartedRecordings().isEmpty()
    744                 && mDataManager.getNonStartedScheduledRecordings().isEmpty()
    745                 && mDataManager.getSeriesRecordings().isEmpty());
    746     }
    747 
    748     @WorkerThread
    749     @VisibleForTesting
    750     // Should be public to use mock DvrManager object.
    751     public void addListener(Listener listener, @NonNull Handler handler) {
    752         SoftPreconditions.checkNotNull(handler);
    753         synchronized (mListener) {
    754             mListener.put(listener, handler);
    755         }
    756     }
    757 
    758     @WorkerThread
    759     @VisibleForTesting
    760     // Should be public to use mock DvrManager object.
    761     public void removeListener(Listener listener) {
    762         synchronized (mListener) {
    763             mListener.remove(listener);
    764         }
    765     }
    766 
    767     /**
    768      * Returns ScheduledRecording.builder based on {@code program}. If program is already started,
    769      * recording started time is clipped to the current time.
    770      */
    771     private ScheduledRecording.Builder createScheduledRecordingBuilder(String inputId,
    772             Program program) {
    773         ScheduledRecording.Builder builder = ScheduledRecording.builder(inputId, program);
    774         long time = System.currentTimeMillis();
    775         if (program.getStartTimeUtcMillis() < time && time < program.getEndTimeUtcMillis()) {
    776             builder.setStartTimeMs(time);
    777         }
    778         return builder;
    779     }
    780 
    781     /**
    782      * Returns a schedule which matches to the given episode.
    783      */
    784     public ScheduledRecording getScheduledRecording(String title, String seasonNumber,
    785             String episodeNumber) {
    786         if (!SoftPreconditions.checkState(mDataManager.isInitialized()) || title == null
    787                 || seasonNumber == null || episodeNumber == null) {
    788             return null;
    789         }
    790         for (ScheduledRecording r : mDataManager.getAllScheduledRecordings()) {
    791             if (title.equals(r.getProgramTitle())
    792                     && seasonNumber.equals(r.getSeasonNumber())
    793                     && episodeNumber.equals(r.getEpisodeNumber())) {
    794                 return r;
    795             }
    796         }
    797         return null;
    798     }
    799 
    800     /**
    801      * Returns a recorded program which is the same episode as the given {@code program}.
    802      */
    803     public RecordedProgram getRecordedProgram(String title, String seasonNumber,
    804             String episodeNumber) {
    805         if (!SoftPreconditions.checkState(mDataManager.isInitialized()) || title == null
    806                 || seasonNumber == null || episodeNumber == null) {
    807             return null;
    808         }
    809         for (RecordedProgram r : mDataManager.getRecordedPrograms()) {
    810             if (title.equals(r.getTitle())
    811                     && seasonNumber.equals(r.getSeasonNumber())
    812                     && episodeNumber.equals(r.getEpisodeNumber())
    813                     && !r.isClipped()) {
    814                 return r;
    815             }
    816         }
    817         return null;
    818     }
    819 
    820     @WorkerThread
    821     private void removeRecordedData(Uri dataUri) {
    822         try {
    823             if (dataUri != null && ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())
    824                     && dataUri.getPath() != null) {
    825                 File recordedProgramPath = new File(dataUri.getPath());
    826                 if (!recordedProgramPath.exists()) {
    827                     if (DEBUG) Log.d(TAG, "File to delete not exist: " + recordedProgramPath);
    828                 } else {
    829                     Utils.deleteDirOrFile(recordedProgramPath);
    830                     if (DEBUG) {
    831                         Log.d(TAG, "Sucessfully deleted files of the recorded program: " + dataUri);
    832                     }
    833                 }
    834             }
    835         } catch (SecurityException e) {
    836             if (DEBUG) {
    837                 Log.d(TAG, "To delete this recorded program, please manually delete video data at"
    838                         + "\nadb shell rm -rf " + dataUri);
    839             }
    840         }
    841     }
    842 
    843     /**
    844      * Remove all the records related to the input.
    845      * <p>
    846      * Note that this should be called after the input was removed.
    847      */
    848     public void forgetStorage(String inputId) {
    849         if (mDataManager.isInitialized()) {
    850             mDataManager.forgetStorage(inputId);
    851         }
    852     }
    853 
    854     /**
    855      * Listener to stop recording request. Should only be internally used inside dvr and its
    856      * sub-package.
    857      */
    858     public interface Listener {
    859         void onStopRecordingRequested(ScheduledRecording scheduledRecording);
    860     }
    861 }
    862