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
17package com.android.tv.dvr;
18
19import android.annotation.TargetApi;
20import android.content.ContentProviderOperation;
21import android.content.ContentResolver;
22import android.content.ContentUris;
23import android.content.Context;
24import android.content.OperationApplicationException;
25import android.media.tv.TvContract;
26import android.media.tv.TvInputInfo;
27import android.net.Uri;
28import android.os.AsyncTask;
29import android.os.Build;
30import android.os.Handler;
31import android.os.RemoteException;
32import android.support.annotation.MainThread;
33import android.support.annotation.NonNull;
34import android.support.annotation.Nullable;
35import android.support.annotation.VisibleForTesting;
36import android.support.annotation.WorkerThread;
37import android.util.Log;
38import android.util.Range;
39
40import com.android.tv.ApplicationSingletons;
41import com.android.tv.TvApplication;
42import com.android.tv.common.SoftPreconditions;
43import com.android.tv.common.feature.CommonFeatures;
44import com.android.tv.data.Channel;
45import com.android.tv.data.Program;
46import com.android.tv.dvr.DvrDataManager.OnRecordedProgramLoadFinishedListener;
47import com.android.tv.dvr.DvrDataManager.RecordedProgramListener;
48import com.android.tv.dvr.DvrScheduleManager.OnInitializeListener;
49import com.android.tv.dvr.SeriesRecording.SeriesState;
50import com.android.tv.util.AsyncDbTask;
51import com.android.tv.util.Utils;
52
53import java.io.File;
54import java.util.ArrayList;
55import java.util.Arrays;
56import java.util.Collections;
57import java.util.HashMap;
58import java.util.List;
59import java.util.Map;
60import 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)
68public 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
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
137    private void createSeriesRecordingsForRecordedProgramsIfNeeded(
138            List<RecordedProgram> recordedPrograms) {
139        for (RecordedProgram recordedProgram : recordedPrograms) {
140            createSeriesRecordingForRecordedProgramIfNeeded(recordedProgram);
141        }
142    }
143
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     */
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     */
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
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     */
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     */
220    public void addSchedule(ScheduledRecording schedule) {
221        if (mDataManager.isDvrScheduleLoadFinished()) {
222            mDataManager.addScheduledRecording(schedule);
223        }
224    }
225
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     */
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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.
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.
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     */
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     */
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     */
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
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     */
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 {
846        void onStopRecordingRequested(ScheduledRecording scheduledRecording);
847    }
848}
849