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.ContentValues;
20 import android.database.Cursor;
21 import android.support.annotation.IntDef;
22 import android.support.annotation.VisibleForTesting;
23 import android.util.Range;
24 
25 import com.android.tv.common.SoftPreconditions;
26 import com.android.tv.data.Channel;
27 import com.android.tv.data.Program;
28 import com.android.tv.dvr.provider.DvrContract;
29 import com.android.tv.util.Utils;
30 
31 import java.lang.annotation.Retention;
32 import java.lang.annotation.RetentionPolicy;
33 import java.util.Comparator;
34 
35 /**
36  * A data class for one recording contents.
37  */
38 @VisibleForTesting
39 public final class ScheduledRecording {
40     private static final String TAG = "Recording";
41 
42     public static final String RECORDING_ID_EXTRA = "extra.dvr.recording.id";  //TODO(DVR) move
43     public static final String PARAM_INPUT_ID = "input_id";
44 
45     public static final long ID_NOT_SET = -1;
46 
47     public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR = new Comparator<ScheduledRecording>() {
48         @Override
49         public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
50             return Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs);
51         }
52     };
53 
54     public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR = new Comparator<ScheduledRecording>() {
55         @Override
56         public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
57             int value = Long.compare(lhs.mPriority, rhs.mPriority);
58             if (value == 0) {
59                 value = Long.compare(lhs.mId, rhs.mId);
60             }
61             return value;
62         }
63     };
64 
65     public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_COMPARATOR
66             = new Comparator<ScheduledRecording>() {
67         @Override
68         public int compare(ScheduledRecording lhs, ScheduledRecording rhs) {
69             int value = START_TIME_COMPARATOR.compare(lhs, rhs);
70             if (value == 0) {
71                 value = PRIORITY_COMPARATOR.compare(lhs, rhs);
72             }
73             return value;
74         }
75     };
76 
builder(Program p)77     public static Builder builder(Program p) {
78         return new Builder()
79                 .setStartTime(p.getStartTimeUtcMillis()).setEndTime(p.getEndTimeUtcMillis())
80                 .setProgramId(p.getId())
81                 .setType(TYPE_PROGRAM);
82     }
83 
builder(long startTime, long endTime)84     public static Builder builder(long startTime, long endTime) {
85         return new Builder()
86                 .setStartTime(startTime)
87                 .setEndTime(endTime)
88                 .setType(TYPE_TIMED);
89     }
90 
91     public static final class Builder {
92         private long mId = ID_NOT_SET;
93         private long mPriority = Long.MAX_VALUE;
94         private long mChannelId;
95         private long mProgramId = ID_NOT_SET;
96         private @RecordingType int mType;
97         private long mStartTime;
98         private long mEndTime;
99         private @RecordingState int mState;
100         private SeasonRecording mParentSeasonRecording;
101 
Builder()102         private Builder() { }
103 
setId(long id)104         public Builder setId(long id) {
105             mId = id;
106             return this;
107         }
108 
setPriority(long priority)109         public Builder setPriority(long priority) {
110             mPriority = priority;
111             return this;
112         }
113 
setChannelId(long channelId)114         public Builder setChannelId(long channelId) {
115             mChannelId = channelId;
116             return this;
117         }
118 
setProgramId(long programId)119         public Builder setProgramId(long programId) {
120             mProgramId = programId;
121             return this;
122         }
123 
setType(@ecordingType int type)124         private Builder setType(@RecordingType int type) {
125             mType = type;
126             return this;
127         }
128 
setStartTime(long startTime)129         public Builder setStartTime(long startTime) {
130             mStartTime = startTime;
131             return this;
132         }
133 
setEndTime(long endTime)134         public Builder setEndTime(long endTime) {
135             mEndTime = endTime;
136             return this;
137         }
138 
setState(@ecordingState int state)139         public Builder setState(@RecordingState int state) {
140             mState = state;
141             return this;
142         }
143 
setParentSeasonRecording(SeasonRecording parentSeasonRecording)144         public Builder setParentSeasonRecording(SeasonRecording parentSeasonRecording) {
145             mParentSeasonRecording = parentSeasonRecording;
146             return this;
147         }
148 
build()149         public ScheduledRecording build() {
150             return new ScheduledRecording(mId, mPriority, mChannelId, mProgramId, mType, mStartTime,
151                     mEndTime, mState, mParentSeasonRecording);
152         }
153     }
154 
155     /**
156      * Creates {@link Builder} object from the given original {@code Recording}.
157      */
buildFrom(ScheduledRecording orig)158     public static Builder buildFrom(ScheduledRecording orig) {
159         return new Builder()
160                 .setId(orig.mId).setChannelId(orig.mChannelId)
161                 .setEndTime(orig.mEndTimeMs).setParentSeasonRecording(orig.mParentSeasonRecording)
162                 .setProgramId(orig.mProgramId)
163                 .setStartTime(orig.mStartTimeMs).setState(orig.mState).setType(orig.mType);
164     }
165 
166     @Retention(RetentionPolicy.SOURCE)
167     @IntDef({STATE_RECORDING_NOT_STARTED, STATE_RECORDING_IN_PROGRESS,
168         STATE_RECORDING_UNEXPECTEDLY_STOPPED, STATE_RECORDING_FINISHED, STATE_RECORDING_FAILED})
169     public @interface RecordingState {}
170     public static final int STATE_RECORDING_NOT_STARTED = 0;
171     public static final int STATE_RECORDING_IN_PROGRESS = 1;
172     @Deprecated // It is not used.
173     public static final int STATE_RECORDING_UNEXPECTEDLY_STOPPED = 2;
174     public static final int STATE_RECORDING_FINISHED = 3;
175     public static final int STATE_RECORDING_FAILED = 4;
176 
177     @Retention(RetentionPolicy.SOURCE)
178     @IntDef({TYPE_TIMED, TYPE_PROGRAM})
179     public @interface RecordingType {}
180     /**
181      * Record with given time range.
182      */
183     static final int TYPE_TIMED = 1;
184     /**
185      * Record with a given program.
186      */
187     static final int TYPE_PROGRAM = 2;
188 
189     @RecordingType private final int mType;
190 
191     /**
192      * Use this projection if you want to create {@link ScheduledRecording} object using {@link #fromCursor}.
193      */
194     public static final String[] PROJECTION = {
195             // Columns must match what is read in Recording.fromCursor()
196             DvrContract.Recordings._ID,
197             DvrContract.Recordings.COLUMN_PRIORITY,
198             DvrContract.Recordings.COLUMN_TYPE,
199             DvrContract.Recordings.COLUMN_CHANNEL_ID,
200             DvrContract.Recordings.COLUMN_PROGRAM_ID,
201             DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS,
202             DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS,
203             DvrContract.Recordings.COLUMN_STATE};
204     /**
205      * Creates {@link ScheduledRecording} object from the given {@link Cursor}.
206      */
fromCursor(Cursor c)207     public static ScheduledRecording fromCursor(Cursor c) {
208         int index = -1;
209         return new Builder()
210                 .setId(c.getLong(++index))
211                 .setPriority(c.getLong(++index))
212                 .setType(recordingType(c.getString(++index)))
213                 .setChannelId(c.getLong(++index))
214                 .setProgramId(c.getLong(++index))
215                 .setStartTime(c.getLong(++index))
216                 .setEndTime(c.getLong(++index))
217                 .setState(recordingState(c.getString(++index)))
218                 .build();
219     }
220 
toContentValues(ScheduledRecording r)221     public static ContentValues toContentValues(ScheduledRecording r) {
222         ContentValues values = new ContentValues();
223         values.put(DvrContract.Recordings.COLUMN_CHANNEL_ID, r.getChannelId());
224         values.put(DvrContract.Recordings.COLUMN_PROGRAM_ID, r.getProgramId());
225         values.put(DvrContract.Recordings.COLUMN_PRIORITY, r.getPriority());
226         values.put(DvrContract.Recordings.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs());
227         values.put(DvrContract.Recordings.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs());
228         values.put(DvrContract.Recordings.COLUMN_STATE, r.getState());
229         values.put(DvrContract.Recordings.COLUMN_TYPE, r.getType());
230         return values;
231     }
232 
233     /**
234      * The ID internal to Live TV
235      */
236     private final long mId;
237 
238     /**
239      * The priority of this recording.
240      *
241      * <p> The lowest number is recorded first. If there is a tie in priority then the lower id
242      * wins.
243      */
244     private final long mPriority;
245 
246 
247     private final long mChannelId;
248     /**
249      * Optional id of the associated program.
250      *
251      */
252     private final long mProgramId;
253 
254     private final long mStartTimeMs;
255     private final long mEndTimeMs;
256     @RecordingState private final int mState;
257 
258     private final SeasonRecording mParentSeasonRecording;
259 
ScheduledRecording(long id, long priority, long channelId, long programId, @RecordingType int type, long startTime, long endTime, @RecordingState int state, SeasonRecording parentSeasonRecording)260     private ScheduledRecording(long id, long priority, long channelId, long programId,
261             @RecordingType int type, long startTime, long endTime,
262             @RecordingState int state, SeasonRecording parentSeasonRecording) {
263         mId = id;
264         mPriority = priority;
265         mChannelId = channelId;
266         mProgramId = programId;
267         mType = type;
268         mStartTimeMs = startTime;
269         mEndTimeMs = endTime;
270         mState = state;
271         mParentSeasonRecording = parentSeasonRecording;
272     }
273 
274     /**
275      * Returns recording schedule type. The possible types are {@link #TYPE_PROGRAM} and
276      * {@link #TYPE_TIMED}.
277      */
278     @RecordingType
getType()279     public int getType() {
280         return mType;
281     }
282 
283     /**
284      * Returns recorded {@link Channel}.
285      */
getChannelId()286     public long getChannelId() {
287         return mChannelId;
288     }
289 
290     /**
291      * Return the optional program id
292      */
getProgramId()293     public long getProgramId() {
294         return mProgramId;
295     }
296 
297     /**
298      * Returns started time.
299      */
getStartTimeMs()300     public long getStartTimeMs() {
301         return mStartTimeMs;
302     }
303 
304     /**
305      * Returns ended time.
306      */
getEndTimeMs()307     public long getEndTimeMs() {
308         return mEndTimeMs;
309     }
310 
311     /**
312      * Returns duration.
313      */
getDuration()314     public long getDuration() {
315         return mEndTimeMs - mStartTimeMs;
316     }
317 
318     /**
319      * Returns the state. The possible states are {@link #STATE_RECORDING_FINISHED},
320      * {@link #STATE_RECORDING_IN_PROGRESS} and {@link #STATE_RECORDING_UNEXPECTEDLY_STOPPED}.
321      */
getState()322     @RecordingState public int getState() {
323         return mState;
324     }
325 
326     /**
327      * Returns {@link SeasonRecording} including this schedule.
328      */
getParentSeasonRecording()329     public SeasonRecording getParentSeasonRecording() {
330         return mParentSeasonRecording;
331     }
332 
getId()333     public long getId() {
334         return mId;
335     }
336 
getPriority()337     public long getPriority() {
338         return mPriority;
339     }
340 
341     /**
342      * Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}.
343      */
recordingType(String type)344     private static @RecordingType int recordingType(String type) {
345         int t;
346         try {
347             t = Integer.valueOf(type);
348         } catch (NullPointerException | NumberFormatException e) {
349             SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type);
350             return TYPE_TIMED;
351         }
352         switch (t) {
353             case TYPE_TIMED:
354                 return TYPE_TIMED;
355             case TYPE_PROGRAM:
356                 return TYPE_PROGRAM;
357             default:
358                 SoftPreconditions.checkArgument(false, TAG, "Unknown recording type " + type);
359                 return TYPE_TIMED;
360         }
361     }
362 
363     /**
364      * Converts a string to a @RecordingState int, defaulting to
365      * {@link #STATE_RECORDING_NOT_STARTED}.
366      */
recordingState(String state)367     private static @RecordingState int recordingState(String state) {
368         int s;
369         try {
370             s = Integer.valueOf(state);
371         } catch (NullPointerException | NumberFormatException e) {
372             SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state);
373             return STATE_RECORDING_NOT_STARTED;
374         }
375         switch (s) {
376             case STATE_RECORDING_NOT_STARTED:
377                 return STATE_RECORDING_NOT_STARTED;
378             case STATE_RECORDING_IN_PROGRESS:
379                 return STATE_RECORDING_IN_PROGRESS;
380             case STATE_RECORDING_FINISHED:
381                 return STATE_RECORDING_FINISHED;
382             case STATE_RECORDING_UNEXPECTEDLY_STOPPED:
383                 return STATE_RECORDING_UNEXPECTEDLY_STOPPED;
384             case STATE_RECORDING_FAILED:
385                 return STATE_RECORDING_FAILED;
386             default:
387                 SoftPreconditions.checkArgument(false, TAG, "Unknown recording state" + state);
388                 return STATE_RECORDING_NOT_STARTED;
389         }
390     }
391 
392     /**
393      * Checks if the {@code period} overlaps with the recording time.
394      */
isOverLapping(Range<Long> period)395     public boolean isOverLapping(Range<Long> period) {
396         return mStartTimeMs <= period.getUpper() && mEndTimeMs >= period.getLower();
397     }
398 
399     @Override
toString()400     public String toString() {
401         return "ScheduledRecording[" + mId
402                 + "]"
403                 + "(startTime=" + Utils.toIsoDateTimeString(mStartTimeMs)
404                 + ",endTime=" + Utils.toIsoDateTimeString(mEndTimeMs)
405                 + ",state=" + mState
406                 + ",priority=" + mPriority
407                 + ")";
408     }
409 }
410