• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
DvrManager(Context context)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 
createSeriesRecordingsForRecordedProgramsIfNeeded( List<RecordedProgram> recordedPrograms)144     private void createSeriesRecordingsForRecordedProgramsIfNeeded(
145             List<RecordedProgram> recordedPrograms) {
146         for (RecordedProgram recordedProgram : recordedPrograms) {
147             createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram);
148         }
149     }
150 
createSeriesRecordingForRecordedProgramIfNeeded(RecordedProgram recordedProgram)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}. */
addSchedule(Program 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      */
addScheduleWithHighestPriority(Program program)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 
addSchedule(Program program, long priority)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. */
addSchedule(Channel channel, long startTime, long endTime)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. */
addSchedule(ScheduledRecording schedule)237     public void addSchedule(ScheduledRecording schedule) {
238         if (mDataManager.isDvrScheduleLoadFinished()) {
239             mDataManager.addScheduledRecording(schedule);
240         }
241     }
242 
addScheduleInternal(String inputId, long channelId, long startTime, long endTime)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. */
addSeriesRecording( Program selectedProgram, List<Program> programsToSchedule, @SeriesRecording.SeriesState int initialState)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 
addSeriesRecording(RecordedProgram recordedProgram)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 
addRecordedProgramToSeriesRecording(SeriesRecording series)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      */
addScheduleToSeriesRecording( SeriesRecording series, List<Program> programsToSchedule)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. */
updateSeriesRecording(SeriesRecording series)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      */
removeSeriesRecording(long seriesRecordingId)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 */
stopRecording(final ScheduledRecording recording)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. */
removeScheduledRecording(ScheduledRecording... schedules)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. */
forceRemoveScheduledRecording(ScheduledRecording... schedules)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. */
removeRecordedProgram(Uri recordedProgramUri)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. */
removeRecordedProgram(long recordedProgramId)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. */
removeRecordedProgram(final RecordedProgram recordedProgram)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 
removeRecordedPrograms(List<Long> recordedProgramIds)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. */
updateScheduledRecording(ScheduledRecording 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      */
getConflictingSchedules(Program program)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      */
getConflictingSchedules( long channelId, long startTimeMs, long endTimeMs)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      */
isConflicting(ScheduledRecording schedule)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      */
getConflictingSchedulesForTune(long channelId)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. */
setHighestPriority(ScheduledRecording 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}. */
suggestHighestPriority(ScheduledRecording 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      */
isChannelRecordable(Channel channel)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      */
isProgramRecordable(Program program)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      */
getCurrentRecording(long channelId)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      */
getAvailableScheduledRecording(long seriesRecordingId)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
getSeriesRecording(Program program)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      */
hasValidItems()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.
addListener(Listener listener, @NonNull Handler handler)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.
removeListener(Listener listener)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      */
createScheduledRecordingBuilder( String inputId, Program program)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. */
getScheduledRecording( String title, String seasonNumber, String episodeNumber)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}. */
getRecordedProgram( String title, String seasonNumber, String episodeNumber)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
removeRecordedData(Uri dataUri)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      */
forgetStorage(String inputId)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 {
onStopRecordingRequested(ScheduledRecording scheduledRecording)872         void onStopRecordingRequested(ScheduledRecording scheduledRecording);
873     }
874 }
875