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