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