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