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