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