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