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