1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.dvr;
18 
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.media.tv.TvInputInfo;
22 import android.os.Handler;
23 import android.support.annotation.MainThread;
24 import android.support.annotation.NonNull;
25 import android.support.annotation.WorkerThread;
26 import android.util.Log;
27 import android.util.Range;
28 import android.widget.Toast;
29 
30 import com.android.tv.ApplicationSingletons;
31 import com.android.tv.TvApplication;
32 import com.android.tv.common.SoftPreconditions;
33 import com.android.tv.common.feature.CommonFeatures;
34 import com.android.tv.common.recording.RecordedProgram;
35 import com.android.tv.data.Channel;
36 import com.android.tv.data.ChannelDataManager;
37 import com.android.tv.data.Program;
38 import com.android.tv.util.AsyncDbTask;
39 import com.android.tv.util.Utils;
40 
41 import java.util.Collections;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 import 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
52 public 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 
DvrManager(Context context)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      */
addSchedule(Program program, List<ScheduledRecording> recordingsToOverride)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      */
addSchedule(Channel channel, long startTime, long endTime)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      */
addSeasonSchedule(Program program)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      */
stopRecording(final ScheduledRecording recording)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      */
removeScheduledRecording(ScheduledRecording scheduledRecording)129     public void removeScheduledRecording(ScheduledRecording scheduledRecording) {
130         Log.i(TAG, "Removing " + scheduledRecording);
131         mDataManager.removeScheduledRecording(scheduledRecording);
132     }
133 
removeRecordedProgram(final RecordedProgram recordedProgram)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      */
getScheduledRecordingsThatConflict(Program program)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
getPeriod(Program program)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      */
canTuneTo(Channel channel, List<ScheduledRecording> outConflictScheduledRecordings)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      */
canRecord(String inputId)200     public boolean canRecord(String inputId) {
201         TvInputInfo info = mDvrSessionManager.getTvInputInfo(inputId);
202         return info != null && info.getTunerCount() > 0;
203     }
204 
205     @WorkerThread
addListener(Listener listener, @NonNull Handler handler)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
removeListener(Listener listener)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 {
onStopRecordingRequested(ScheduledRecording scheduledRecording)224         void onStopRecordingRequested(ScheduledRecording scheduledRecording);
225     }
226 }
227