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.content.ContentResolver;
20import android.content.Context;
21import android.media.tv.TvInputInfo;
22import android.os.Handler;
23import android.support.annotation.MainThread;
24import android.support.annotation.NonNull;
25import android.support.annotation.WorkerThread;
26import android.util.Log;
27import android.util.Range;
28import android.widget.Toast;
29
30import com.android.tv.ApplicationSingletons;
31import com.android.tv.TvApplication;
32import com.android.tv.common.SoftPreconditions;
33import com.android.tv.common.feature.CommonFeatures;
34import com.android.tv.common.recording.RecordedProgram;
35import com.android.tv.data.Channel;
36import com.android.tv.data.ChannelDataManager;
37import com.android.tv.data.Program;
38import com.android.tv.util.AsyncDbTask;
39import com.android.tv.util.Utils;
40
41import java.util.Collections;
42import java.util.HashMap;
43import java.util.List;
44import java.util.Map;
45import java.util.Map.Entry;
46
47/**
48 * DVR manager class to add and remove recordings. UI can modify recording list through this class,
49 * instead of modifying them directly through {@link DvrDataManager}.
50 */
51@MainThread
52public class DvrManager {
53    private final static String TAG = "DvrManager";
54    private final WritableDvrDataManager mDataManager;
55    private final ChannelDataManager mChannelDataManager;
56    private final DvrSessionManager mDvrSessionManager;
57    // @GuardedBy("mListener")
58    private final Map<Listener, Handler> mListener = new HashMap<>();
59    private final Context mAppContext;
60
61    public DvrManager(Context context) {
62        SoftPreconditions.checkFeatureEnabled(context, CommonFeatures.DVR, TAG);
63        ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
64        mDataManager = (WritableDvrDataManager) appSingletons.getDvrDataManager();
65        mAppContext = context.getApplicationContext();
66        mChannelDataManager = appSingletons.getChannelDataManager();
67        mDvrSessionManager = appSingletons.getDvrSessionManger();
68    }
69
70    /**
71     * Schedules a recording for {@code program} instead of the list of recording that conflict.
72     * @param program the program to record
73     * @param recordingsToOverride the possible empty list of recordings that will not be recorded
74     */
75    public void addSchedule(Program program, List<ScheduledRecording> recordingsToOverride) {
76        Log.i(TAG,
77                "Adding scheduled recording of " + program + " instead of " + recordingsToOverride);
78        Collections.sort(recordingsToOverride, ScheduledRecording.PRIORITY_COMPARATOR);
79        Channel c = mChannelDataManager.getChannel(program.getChannelId());
80        long priority = recordingsToOverride.isEmpty() ? Long.MAX_VALUE
81                : recordingsToOverride.get(0).getPriority() - 1;
82        ScheduledRecording r = ScheduledRecording.builder(program)
83                .setPriority(priority)
84                .setChannelId(c.getId())
85                .build();
86        mDataManager.addScheduledRecording(r);
87    }
88
89    /**
90     * Adds a recording schedule with a time range.
91     */
92    public void addSchedule(Channel channel, long startTime, long endTime) {
93        Log.i(TAG, "Adding scheduled recording of channel" + channel + " starting at " +
94                Utils.toTimeString(startTime) + " and ending at " + Utils.toTimeString(endTime));
95        //TODO: handle error cases
96        ScheduledRecording r = ScheduledRecording.builder(startTime, endTime)
97                .setChannelId(channel.getId())
98                .build();
99        mDataManager.addScheduledRecording(r);
100    }
101
102    /**
103     * Adds a season recording schedule based on {@code program}.
104     */
105    public void addSeasonSchedule(Program program) {
106        Log.i(TAG, "Adding season recording of " + program);
107        // TODO: implement
108    }
109
110    /**
111     * Stops the currently recorded program
112     */
113    public void stopRecording(final ScheduledRecording recording) {
114        synchronized (mListener) {
115            for (final Entry<Listener, Handler> entry : mListener.entrySet()) {
116                entry.getValue().post(new Runnable() {
117                    @Override
118                    public void run() {
119                        entry.getKey().onStopRecordingRequested(recording);
120                    }
121                });
122            }
123        }
124    }
125
126    /**
127     * Removes a scheduled recording or an existing recording.
128     */
129    public void removeScheduledRecording(ScheduledRecording scheduledRecording) {
130        Log.i(TAG, "Removing " + scheduledRecording);
131        mDataManager.removeScheduledRecording(scheduledRecording);
132    }
133
134    public void removeRecordedProgram(final RecordedProgram recordedProgram) {
135        // TODO(dvr): implement
136        Log.i(TAG, "To delete " + recordedProgram
137                + "\nyou should manually delete video data at"
138                + "\nadb shell rm -rf " + recordedProgram.getDataUri()
139        );
140        Toast.makeText(mAppContext, "Deleting recorded programs is not fully implemented yet",
141                Toast.LENGTH_SHORT).show();
142        new AsyncDbTask<Void, Void, Void>() {
143            @Override
144            protected Void doInBackground(Void... params) {
145                ContentResolver resolver = mAppContext.getContentResolver();
146                resolver.delete(recordedProgram.getUri(), null, null);
147                return null;
148            }
149        }.execute();
150    }
151
152    /**
153     * Returns priority ordered list of all scheduled recording that will not be recorded if
154     * this program is.
155     *
156     * <p>Any empty list means there is no conflicts.  If there is conflict the program must be
157     * scheduled to record with a Priority lower than the first Recording in the list returned.
158     */
159    public List<ScheduledRecording> getScheduledRecordingsThatConflict(Program program) {
160        //TODO(DVR): move to scheduler.
161        //TODO(DVR): deal with more than one DvrInputService
162        List<ScheduledRecording> overLap = mDataManager.getRecordingsThatOverlapWith(getPeriod(program));
163        if (!overLap.isEmpty()) {
164            // TODO(DVR): ignore shows that already won't record.
165            Channel channel = mChannelDataManager.getChannel(program.getChannelId());
166            if (channel != null) {
167                TvInputInfo info = mDvrSessionManager.getTvInputInfo(channel.getInputId());
168                if (info == null) {
169                    Log.w(TAG,
170                            "Could not find a recording TvInputInfo for " + channel.getInputId());
171                    return overLap;
172                }
173                int remove = Math.max(0, info.getTunerCount() - 1);
174                if (remove >= overLap.size()) {
175                    return Collections.EMPTY_LIST;
176                }
177                overLap = overLap.subList(remove, overLap.size() - 1);
178            }
179        }
180        return overLap;
181    }
182
183    @NonNull
184    private static Range getPeriod(Program program) {
185        return new Range(program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis());
186    }
187
188    /**
189     * Checks whether {@code channel} can be tuned without any conflict with existing recordings
190     * in progress. If there is any conflict, {@code outConflictRecordings} will be filled.
191     */
192    public boolean canTuneTo(Channel channel, List<ScheduledRecording> outConflictScheduledRecordings) {
193        // TODO: implement
194        return true;
195    }
196
197    /**
198     * Returns true is the inputId supports recording.
199     */
200    public boolean canRecord(String inputId) {
201        TvInputInfo info = mDvrSessionManager.getTvInputInfo(inputId);
202        return info != null && info.getTunerCount() > 0;
203    }
204
205    @WorkerThread
206    void addListener(Listener listener, @NonNull Handler handler) {
207        SoftPreconditions.checkNotNull(handler);
208        synchronized (mListener) {
209            mListener.put(listener, handler);
210        }
211    }
212
213    @WorkerThread
214    void removeListener(Listener listener) {
215        synchronized (mListener) {
216            mListener.remove(listener);
217        }
218    }
219
220    /**
221     * Listener internally used inside dvr package.
222     */
223    interface Listener {
224        void onStopRecordingRequested(ScheduledRecording scheduledRecording);
225    }
226}
227