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.data;
18 
19 import android.annotation.TargetApi;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.database.Cursor;
23 import android.os.Build;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 import android.support.annotation.IntDef;
27 import android.support.annotation.Nullable;
28 import android.text.TextUtils;
29 import android.util.Range;
30 
31 import com.android.tv.R;
32 import com.android.tv.TvSingletons;
33 import com.android.tv.common.SoftPreconditions;
34 import com.android.tv.common.util.CommonUtils;
35 import com.android.tv.data.api.Channel;
36 import com.android.tv.data.api.Program;
37 import com.android.tv.dvr.DvrScheduleManager;
38 import com.android.tv.dvr.provider.DvrContract.Schedules;
39 import com.android.tv.util.CompositeComparator;
40 
41 import java.lang.annotation.Retention;
42 import java.lang.annotation.RetentionPolicy;
43 import java.util.Collection;
44 import java.util.Comparator;
45 import java.util.Objects;
46 
47 /** A data class for one recording contents. */
48 @TargetApi(Build.VERSION_CODES.N)
49 @SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
50 public final class ScheduledRecording implements Parcelable {
51     private static final String TAG = "ScheduledRecording";
52 
53     /** Indicates that the ID is not assigned yet. */
54     public static final long ID_NOT_SET = 0;
55 
56     /** The default priority of the recording. */
57     public static final long DEFAULT_PRIORITY = Long.MAX_VALUE >> 1;
58 
59     /** The default offset time of the recording. */
60     public static final long DEFAULT_TIME_OFFSET = 0;
61 
62     /** Compares the start time in ascending order. */
63     public static final Comparator<ScheduledRecording> START_TIME_COMPARATOR =
64             (ScheduledRecording lhs, ScheduledRecording rhs) ->
65                     Long.compare(lhs.mStartTimeMs, rhs.mStartTimeMs);
66 
67     /** Compares the end time in ascending order. */
68     public static final Comparator<ScheduledRecording> END_TIME_COMPARATOR =
69             (ScheduledRecording lhs, ScheduledRecording rhs) ->
70                     Long.compare(lhs.mEndTimeMs, rhs.mEndTimeMs);
71 
72     /** Compares ID in ascending order. The schedule with the larger ID was created later. */
73     public static final Comparator<ScheduledRecording> ID_COMPARATOR =
74             (ScheduledRecording lhs, ScheduledRecording rhs) -> Long.compare(lhs.mId, rhs.mId);
75 
76     /** Compares the priority in ascending order. */
77     public static final Comparator<ScheduledRecording> PRIORITY_COMPARATOR =
78             (ScheduledRecording lhs, ScheduledRecording rhs) ->
79                     Long.compare(lhs.mPriority, rhs.mPriority);
80 
81     /**
82      * Compares start time in ascending order and then priority in descending order and then ID in
83      * descending order.
84      */
85     public static final Comparator<ScheduledRecording> START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR =
86             new CompositeComparator<>(
87                     START_TIME_COMPARATOR,
88                     PRIORITY_COMPARATOR.reversed(),
89                     ID_COMPARATOR.reversed());
90 
91     /** Builds scheduled recordings from programs. */
builder(String inputId, Program p)92     public static Builder builder(String inputId, Program p) {
93         return new Builder()
94                 .setInputId(inputId)
95                 .setChannelId(p.getChannelId())
96                 .setStartTimeMs(p.getStartTimeUtcMillis())
97                 .setEndTimeMs(p.getEndTimeUtcMillis())
98                 .setProgramId(p.getId())
99                 .setProgramTitle(p.getTitle())
100                 .setSeasonNumber(p.getSeasonNumber())
101                 .setEpisodeNumber(p.getEpisodeNumber())
102                 .setEpisodeTitle(p.getEpisodeTitle())
103                 .setProgramDescription(p.getDescription())
104                 .setProgramLongDescription(p.getLongDescription())
105                 .setProgramPosterArtUri(p.getPosterArtUri())
106                 .setProgramThumbnailUri(p.getThumbnailUri())
107                 .setType(TYPE_PROGRAM);
108     }
109 
builder(String inputId, long channelId, long startTime, long endTime)110     public static Builder builder(String inputId, long channelId, long startTime, long endTime) {
111         return new Builder()
112                 .setInputId(inputId)
113                 .setChannelId(channelId)
114                 .setStartTimeMs(startTime)
115                 .setEndTimeMs(endTime)
116                 .setType(TYPE_TIMED);
117     }
118 
119     /** Creates a new Builder with the values set from the {@link RecordedProgram}. */
builder(RecordedProgram p)120     public static Builder builder(RecordedProgram p) {
121         boolean isProgramRecording = !TextUtils.isEmpty(p.getTitle());
122         return new Builder()
123                 .setInputId(p.getInputId())
124                 .setChannelId(p.getChannelId())
125                 .setType(isProgramRecording ? TYPE_PROGRAM : TYPE_TIMED)
126                 .setStartTimeMs(p.getStartTimeUtcMillis())
127                 .setEndTimeMs(p.getEndTimeUtcMillis())
128                 .setProgramTitle(p.getTitle())
129                 .setSeasonNumber(p.getSeasonNumber())
130                 .setEpisodeNumber(p.getEpisodeNumber())
131                 .setEpisodeTitle(p.getEpisodeTitle())
132                 .setProgramDescription(p.getDescription())
133                 .setProgramLongDescription(p.getLongDescription())
134                 .setProgramPosterArtUri(p.getPosterArtUri())
135                 .setProgramThumbnailUri(p.getThumbnailUri())
136                 .setState(STATE_RECORDING_FINISHED)
137                 .setRecordedProgramId(p.getId());
138     }
139 
140     public static final class Builder {
141         private long mId = ID_NOT_SET;
142         private long mPriority = DvrScheduleManager.DEFAULT_PRIORITY;
143         private String mInputId;
144         private long mChannelId;
145         private long mProgramId = ID_NOT_SET;
146         private String mProgramTitle;
147         private @RecordingType int mType;
148         private long mStartTimeMs;
149         private long mEndTimeMs;
150         private String mSeasonNumber;
151         private String mEpisodeNumber;
152         private String mEpisodeTitle;
153         private String mProgramDescription;
154         private String mProgramLongDescription;
155         private String mProgramPosterArtUri;
156         private String mProgramThumbnailUri;
157         private @RecordingState int mState;
158         private long mSeriesRecordingId = ID_NOT_SET;
159         private Long mRecodedProgramId;
160         private Integer mFailedReason;
161         private long mStartOffsetMs = DEFAULT_TIME_OFFSET;
162         private long mEndOffsetMs = DEFAULT_TIME_OFFSET;
163 
Builder()164         private Builder() {}
165 
setId(long id)166         public Builder setId(long id) {
167             mId = id;
168             return this;
169         }
170 
setPriority(long priority)171         public Builder setPriority(long priority) {
172             mPriority = priority;
173             return this;
174         }
175 
setInputId(String inputId)176         public Builder setInputId(String inputId) {
177             mInputId = inputId;
178             return this;
179         }
180 
setChannelId(long channelId)181         public Builder setChannelId(long channelId) {
182             mChannelId = channelId;
183             return this;
184         }
185 
setProgramId(long programId)186         public Builder setProgramId(long programId) {
187             mProgramId = programId;
188             return this;
189         }
190 
setProgramTitle(String programTitle)191         public Builder setProgramTitle(String programTitle) {
192             mProgramTitle = programTitle;
193             return this;
194         }
195 
setType(@ecordingType int type)196         private Builder setType(@RecordingType int type) {
197             mType = type;
198             return this;
199         }
200 
setStartTimeMs(long startTimeMs)201         public Builder setStartTimeMs(long startTimeMs) {
202             mStartTimeMs = startTimeMs;
203             return this;
204         }
205 
setEndTimeMs(long endTimeMs)206         public Builder setEndTimeMs(long endTimeMs) {
207             mEndTimeMs = endTimeMs;
208             return this;
209         }
210 
setSeasonNumber(String seasonNumber)211         public Builder setSeasonNumber(String seasonNumber) {
212             mSeasonNumber = seasonNumber;
213             return this;
214         }
215 
setEpisodeNumber(String episodeNumber)216         public Builder setEpisodeNumber(String episodeNumber) {
217             mEpisodeNumber = episodeNumber;
218             return this;
219         }
220 
setEpisodeTitle(String episodeTitle)221         public Builder setEpisodeTitle(String episodeTitle) {
222             mEpisodeTitle = episodeTitle;
223             return this;
224         }
225 
setProgramDescription(String description)226         public Builder setProgramDescription(String description) {
227             mProgramDescription = description;
228             return this;
229         }
230 
setProgramLongDescription(String longDescription)231         public Builder setProgramLongDescription(String longDescription) {
232             mProgramLongDescription = longDescription;
233             return this;
234         }
235 
setProgramPosterArtUri(String programPosterArtUri)236         public Builder setProgramPosterArtUri(String programPosterArtUri) {
237             mProgramPosterArtUri = programPosterArtUri;
238             return this;
239         }
240 
setProgramThumbnailUri(String programThumbnailUri)241         public Builder setProgramThumbnailUri(String programThumbnailUri) {
242             mProgramThumbnailUri = programThumbnailUri;
243             return this;
244         }
245 
setState(@ecordingState int state)246         public Builder setState(@RecordingState int state) {
247             mState = state;
248             return this;
249         }
250 
setSeriesRecordingId(long seriesRecordingId)251         public Builder setSeriesRecordingId(long seriesRecordingId) {
252             mSeriesRecordingId = seriesRecordingId;
253             return this;
254         }
255 
setRecordedProgramId(Long recordedProgramId)256         public Builder setRecordedProgramId(Long recordedProgramId) {
257             mRecodedProgramId = recordedProgramId;
258             return this;
259         }
260 
setFailedReason(Integer reason)261         public Builder setFailedReason(Integer reason) {
262             mFailedReason = reason;
263             return this;
264         }
265 
setStartOffsetMs(long startOffsetMs)266         public Builder setStartOffsetMs(long startOffsetMs) {
267             mStartOffsetMs = Math.max(0, startOffsetMs);
268             return this;
269         }
270 
setEndOffsetMs(long endOffsetMs)271         public Builder setEndOffsetMs(long endOffsetMs) {
272             mEndOffsetMs = Math.max(0, endOffsetMs);
273             return this;
274         }
275 
build()276         public ScheduledRecording build() {
277             return new ScheduledRecording(
278                     mId,
279                     mPriority,
280                     mInputId,
281                     mChannelId,
282                     mProgramId,
283                     mProgramTitle,
284                     mType,
285                     mStartTimeMs,
286                     mEndTimeMs,
287                     mSeasonNumber,
288                     mEpisodeNumber,
289                     mEpisodeTitle,
290                     mProgramDescription,
291                     mProgramLongDescription,
292                     mProgramPosterArtUri,
293                     mProgramThumbnailUri,
294                     mState,
295                     mSeriesRecordingId,
296                     mRecodedProgramId,
297                     mFailedReason,
298                     mStartOffsetMs,
299                     mEndOffsetMs);
300         }
301     }
302 
303     /** Creates {@link Builder} object from the given original {@code Recording}. */
buildFrom(ScheduledRecording orig)304     public static Builder buildFrom(ScheduledRecording orig) {
305         return new Builder()
306                 .setId(orig.mId)
307                 .setInputId(orig.mInputId)
308                 .setChannelId(orig.mChannelId)
309                 .setEndTimeMs(orig.mEndTimeMs)
310                 .setSeriesRecordingId(orig.mSeriesRecordingId)
311                 .setPriority(orig.mPriority)
312                 .setProgramId(orig.mProgramId)
313                 .setProgramTitle(orig.mProgramTitle)
314                 .setStartTimeMs(orig.mStartTimeMs)
315                 .setSeasonNumber(orig.getSeasonNumber())
316                 .setEpisodeNumber(orig.getEpisodeNumber())
317                 .setEpisodeTitle(orig.getEpisodeTitle())
318                 .setProgramDescription(orig.getProgramDescription())
319                 .setProgramLongDescription(orig.getProgramLongDescription())
320                 .setProgramPosterArtUri(orig.getProgramPosterArtUri())
321                 .setProgramThumbnailUri(orig.getProgramThumbnailUri())
322                 .setState(orig.mState)
323                 .setFailedReason(orig.getFailedReason())
324                 .setType(orig.mType)
325                 .setStartOffsetMs(orig.getStartOffsetMs())
326                 .setEndOffsetMs(orig.getEndOffsetMs());
327     }
328 
329     @Retention(RetentionPolicy.SOURCE)
330     @IntDef({
331         STATE_RECORDING_NOT_STARTED,
332         STATE_RECORDING_IN_PROGRESS,
333         STATE_RECORDING_FINISHED,
334         STATE_RECORDING_FAILED,
335         STATE_RECORDING_CLIPPED,
336         STATE_RECORDING_DELETED,
337         STATE_RECORDING_CANCELED
338     })
339     public @interface RecordingState {}
340 
341     public static final int STATE_RECORDING_NOT_STARTED = 0;
342     public static final int STATE_RECORDING_IN_PROGRESS = 1;
343     public static final int STATE_RECORDING_FINISHED = 2;
344     public static final int STATE_RECORDING_FAILED = 3;
345     public static final int STATE_RECORDING_CLIPPED = 4;
346     public static final int STATE_RECORDING_DELETED = 5;
347     public static final int STATE_RECORDING_CANCELED = 6;
348 
349     /** The reasons of failed recordings */
350     @Retention(RetentionPolicy.SOURCE)
351     @IntDef({
352         FAILED_REASON_OTHER,
353         FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED,
354         FAILED_REASON_NOT_FINISHED,
355         FAILED_REASON_SCHEDULER_STOPPED,
356         FAILED_REASON_INVALID_CHANNEL,
357         FAILED_REASON_MESSAGE_NOT_SENT,
358         FAILED_REASON_CONNECTION_FAILED,
359         FAILED_REASON_RESOURCE_BUSY,
360         FAILED_REASON_INPUT_UNAVAILABLE,
361         FAILED_REASON_INPUT_DVR_UNSUPPORTED,
362         FAILED_REASON_INSUFFICIENT_SPACE
363     })
364     public @interface RecordingFailedReason {}
365 
366     // next number for failed reason: 11
367     public static final int FAILED_REASON_OTHER = 0;
368     public static final int FAILED_REASON_NOT_FINISHED = 2;
369     public static final int FAILED_REASON_SCHEDULER_STOPPED = 3;
370     public static final int FAILED_REASON_INVALID_CHANNEL = 4;
371     public static final int FAILED_REASON_MESSAGE_NOT_SENT = 5;
372     public static final int FAILED_REASON_CONNECTION_FAILED = 6;
373 
374     // for the following reasons, show advice to users
375     // TODO(b/72638597): add failure condition of "weak signal"
376 
377     // failed reason is FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED when tuner or external
378     // storage is disconnected
379     public static final int FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED = 1;
380     // failed reason is FAILED_REASON_RESOURCE_BUSY when antenna is disconnected or signal is weak
381     public static final int FAILED_REASON_RESOURCE_BUSY = 7;
382     public static final int FAILED_REASON_INPUT_UNAVAILABLE = 8;
383     public static final int FAILED_REASON_INPUT_DVR_UNSUPPORTED = 9;
384     public static final int FAILED_REASON_INSUFFICIENT_SPACE = 10;
385 
386     @Retention(RetentionPolicy.SOURCE)
387     @IntDef({TYPE_TIMED, TYPE_PROGRAM})
388     public @interface RecordingType {}
389     /** Record with given time range. */
390     public static final int TYPE_TIMED = 1;
391     /** Record with a given program. */
392     public static final int TYPE_PROGRAM = 2;
393 
394     @RecordingType private final int mType;
395 
396     /**
397      * Use this projection if you want to create {@link ScheduledRecording} object using {@link
398      * #fromCursor}.
399      */
400     public static final String[] PROJECTION = {
401         // Columns must match what is read in #fromCursor
402         Schedules._ID,
403         Schedules.COLUMN_PRIORITY,
404         Schedules.COLUMN_TYPE,
405         Schedules.COLUMN_INPUT_ID,
406         Schedules.COLUMN_CHANNEL_ID,
407         Schedules.COLUMN_PROGRAM_ID,
408         Schedules.COLUMN_PROGRAM_TITLE,
409         Schedules.COLUMN_START_TIME_UTC_MILLIS,
410         Schedules.COLUMN_END_TIME_UTC_MILLIS,
411         Schedules.COLUMN_SEASON_NUMBER,
412         Schedules.COLUMN_EPISODE_NUMBER,
413         Schedules.COLUMN_EPISODE_TITLE,
414         Schedules.COLUMN_PROGRAM_DESCRIPTION,
415         Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION,
416         Schedules.COLUMN_PROGRAM_POST_ART_URI,
417         Schedules.COLUMN_PROGRAM_THUMBNAIL_URI,
418         Schedules.COLUMN_STATE,
419         Schedules.COLUMN_FAILED_REASON,
420         Schedules.COLUMN_SERIES_RECORDING_ID
421     };
422 
423     /**
424      * Use this projection if you want to create {@link ScheduledRecording} object using {@link
425      * #fromCursorWithTimeOffset}.
426      */
427     public static final String[] PROJECTION_WITH_TIME_OFFSET = {
428         // Columns must match what is read in #fromCursor
429         Schedules._ID,
430         Schedules.COLUMN_PRIORITY,
431         Schedules.COLUMN_TYPE,
432         Schedules.COLUMN_INPUT_ID,
433         Schedules.COLUMN_CHANNEL_ID,
434         Schedules.COLUMN_PROGRAM_ID,
435         Schedules.COLUMN_PROGRAM_TITLE,
436         Schedules.COLUMN_START_TIME_UTC_MILLIS,
437         Schedules.COLUMN_END_TIME_UTC_MILLIS,
438         Schedules.COLUMN_SEASON_NUMBER,
439         Schedules.COLUMN_EPISODE_NUMBER,
440         Schedules.COLUMN_EPISODE_TITLE,
441         Schedules.COLUMN_PROGRAM_DESCRIPTION,
442         Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION,
443         Schedules.COLUMN_PROGRAM_POST_ART_URI,
444         Schedules.COLUMN_PROGRAM_THUMBNAIL_URI,
445         Schedules.COLUMN_STATE,
446         Schedules.COLUMN_FAILED_REASON,
447         Schedules.COLUMN_SERIES_RECORDING_ID,
448         Schedules.COLUMN_START_OFFSET_MILLIS,
449         Schedules.COLUMN_END_OFFSET_MILLIS
450     };
451 
452     /** Creates {@link ScheduledRecording} object from the given {@link Cursor}. */
fromCursor(Cursor c)453     public static ScheduledRecording fromCursor(Cursor c) {
454         int index = -1;
455         return new Builder()
456                 .setId(c.getLong(++index))
457                 .setPriority(c.getLong(++index))
458                 .setType(recordingType(c.getString(++index)))
459                 .setInputId(c.getString(++index))
460                 .setChannelId(c.getLong(++index))
461                 .setProgramId(c.getLong(++index))
462                 .setProgramTitle(c.getString(++index))
463                 .setStartTimeMs(c.getLong(++index))
464                 .setEndTimeMs(c.getLong(++index))
465                 .setSeasonNumber(c.getString(++index))
466                 .setEpisodeNumber(c.getString(++index))
467                 .setEpisodeTitle(c.getString(++index))
468                 .setProgramDescription(c.getString(++index))
469                 .setProgramLongDescription(c.getString(++index))
470                 .setProgramPosterArtUri(c.getString(++index))
471                 .setProgramThumbnailUri(c.getString(++index))
472                 .setState(recordingState(c.getString(++index)))
473                 .setFailedReason(recordingFailedReason(c.getString(++index)))
474                 .setSeriesRecordingId(c.getLong(++index))
475                 .build();
476     }
477 
478     /** Creates {@link ScheduledRecording} object from the given {@link Cursor}. */
fromCursorWithTimeOffset(Cursor c)479     public static ScheduledRecording fromCursorWithTimeOffset(Cursor c) {
480         int index = -1;
481         return new Builder()
482                 .setId(c.getLong(++index))
483                 .setPriority(c.getLong(++index))
484                 .setType(recordingType(c.getString(++index)))
485                 .setInputId(c.getString(++index))
486                 .setChannelId(c.getLong(++index))
487                 .setProgramId(c.getLong(++index))
488                 .setProgramTitle(c.getString(++index))
489                 .setStartTimeMs(c.getLong(++index))
490                 .setEndTimeMs(c.getLong(++index))
491                 .setSeasonNumber(c.getString(++index))
492                 .setEpisodeNumber(c.getString(++index))
493                 .setEpisodeTitle(c.getString(++index))
494                 .setProgramDescription(c.getString(++index))
495                 .setProgramLongDescription(c.getString(++index))
496                 .setProgramPosterArtUri(c.getString(++index))
497                 .setProgramThumbnailUri(c.getString(++index))
498                 .setState(recordingState(c.getString(++index)))
499                 .setFailedReason(recordingFailedReason(c.getString(++index)))
500                 .setSeriesRecordingId(c.getLong(++index))
501                 .setStartOffsetMs(c.getLong(++index))
502                 .setEndOffsetMs(c.getLong(++index))
503                 .build();
504     }
505 
toContentValues(ScheduledRecording r)506     public static ContentValues toContentValues(ScheduledRecording r) {
507         ContentValues values = new ContentValues();
508         if (r.getId() != ID_NOT_SET) {
509             values.put(Schedules._ID, r.getId());
510         }
511         values.put(Schedules.COLUMN_INPUT_ID, r.getInputId());
512         values.put(Schedules.COLUMN_CHANNEL_ID, r.getChannelId());
513         values.put(Schedules.COLUMN_PROGRAM_ID, r.getProgramId());
514         values.put(Schedules.COLUMN_PROGRAM_TITLE, r.getProgramTitle());
515         values.put(Schedules.COLUMN_PRIORITY, r.getPriority());
516         values.put(Schedules.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs());
517         values.put(Schedules.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs());
518         values.put(Schedules.COLUMN_SEASON_NUMBER, r.getSeasonNumber());
519         values.put(Schedules.COLUMN_EPISODE_NUMBER, r.getEpisodeNumber());
520         values.put(Schedules.COLUMN_EPISODE_TITLE, r.getEpisodeTitle());
521         values.put(Schedules.COLUMN_PROGRAM_DESCRIPTION, r.getProgramDescription());
522         values.put(Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, r.getProgramLongDescription());
523         values.put(Schedules.COLUMN_PROGRAM_POST_ART_URI, r.getProgramPosterArtUri());
524         values.put(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, r.getProgramThumbnailUri());
525         values.put(Schedules.COLUMN_STATE, recordingState(r.getState()));
526         values.put(Schedules.COLUMN_FAILED_REASON, recordingFailedReason(r.getFailedReason()));
527         values.put(Schedules.COLUMN_TYPE, recordingType(r.getType()));
528         if (r.getSeriesRecordingId() != ID_NOT_SET) {
529             values.put(Schedules.COLUMN_SERIES_RECORDING_ID, r.getSeriesRecordingId());
530         } else {
531             values.putNull(Schedules.COLUMN_SERIES_RECORDING_ID);
532         }
533         return values;
534     }
535 
toContentValuesWithTimeOffset(ScheduledRecording r)536     public static ContentValues toContentValuesWithTimeOffset(ScheduledRecording r) {
537         ContentValues values = new ContentValues();
538         if (r.getId() != ID_NOT_SET) {
539             values.put(Schedules._ID, r.getId());
540         }
541         values.put(Schedules.COLUMN_INPUT_ID, r.getInputId());
542         values.put(Schedules.COLUMN_CHANNEL_ID, r.getChannelId());
543         values.put(Schedules.COLUMN_PROGRAM_ID, r.getProgramId());
544         values.put(Schedules.COLUMN_PROGRAM_TITLE, r.getProgramTitle());
545         values.put(Schedules.COLUMN_PRIORITY, r.getPriority());
546         values.put(Schedules.COLUMN_START_TIME_UTC_MILLIS, r.getStartTimeMs());
547         values.put(Schedules.COLUMN_END_TIME_UTC_MILLIS, r.getEndTimeMs());
548         values.put(Schedules.COLUMN_SEASON_NUMBER, r.getSeasonNumber());
549         values.put(Schedules.COLUMN_EPISODE_NUMBER, r.getEpisodeNumber());
550         values.put(Schedules.COLUMN_EPISODE_TITLE, r.getEpisodeTitle());
551         values.put(Schedules.COLUMN_PROGRAM_DESCRIPTION, r.getProgramDescription());
552         values.put(Schedules.COLUMN_PROGRAM_LONG_DESCRIPTION, r.getProgramLongDescription());
553         values.put(Schedules.COLUMN_PROGRAM_POST_ART_URI, r.getProgramPosterArtUri());
554         values.put(Schedules.COLUMN_PROGRAM_THUMBNAIL_URI, r.getProgramThumbnailUri());
555         values.put(Schedules.COLUMN_STATE, recordingState(r.getState()));
556         values.put(Schedules.COLUMN_FAILED_REASON, recordingFailedReason(r.getFailedReason()));
557         values.put(Schedules.COLUMN_TYPE, recordingType(r.getType()));
558         if (r.getSeriesRecordingId() != ID_NOT_SET) {
559             values.put(Schedules.COLUMN_SERIES_RECORDING_ID, r.getSeriesRecordingId());
560         } else {
561             values.putNull(Schedules.COLUMN_SERIES_RECORDING_ID);
562         }
563         values.put(Schedules.COLUMN_START_OFFSET_MILLIS, r.getStartOffsetMs());
564         values.put(Schedules.COLUMN_END_OFFSET_MILLIS, r.getEndOffsetMs());
565         return values;
566     }
567 
fromParcel(Parcel in)568     public static ScheduledRecording fromParcel(Parcel in) {
569         return new Builder()
570                 .setId(in.readLong())
571                 .setPriority(in.readLong())
572                 .setInputId(in.readString())
573                 .setChannelId(in.readLong())
574                 .setProgramId(in.readLong())
575                 .setProgramTitle(in.readString())
576                 .setType(in.readInt())
577                 .setStartTimeMs(in.readLong())
578                 .setEndTimeMs(in.readLong())
579                 .setSeasonNumber(in.readString())
580                 .setEpisodeNumber(in.readString())
581                 .setEpisodeTitle(in.readString())
582                 .setProgramDescription(in.readString())
583                 .setProgramLongDescription(in.readString())
584                 .setProgramPosterArtUri(in.readString())
585                 .setProgramThumbnailUri(in.readString())
586                 .setState(in.readInt())
587                 .setFailedReason(recordingFailedReason(in.readString()))
588                 .setSeriesRecordingId(in.readLong())
589                 .setStartOffsetMs(in.readLong())
590                 .setEndOffsetMs(in.readLong())
591                 .build();
592     }
593 
594     public static final Parcelable.Creator<ScheduledRecording> CREATOR =
595             new Parcelable.Creator<ScheduledRecording>() {
596                 @Override
597                 public ScheduledRecording createFromParcel(Parcel in) {
598                     return ScheduledRecording.fromParcel(in);
599                 }
600 
601                 @Override
602                 public ScheduledRecording[] newArray(int size) {
603                     return new ScheduledRecording[size];
604                 }
605             };
606 
607     /** The ID internal to TV app */
608     private long mId;
609 
610     /**
611      * The priority of this recording.
612      *
613      * <p>The highest number is recorded first. If there is a tie in priority then the higher id
614      * wins.
615      */
616     private final long mPriority;
617 
618     private final String mInputId;
619     private final long mChannelId;
620     /** Optional id of the associated program. */
621     private final long mProgramId;
622 
623     private final String mProgramTitle;
624 
625     private final long mStartTimeMs;
626     private final long mEndTimeMs;
627     private final String mSeasonNumber;
628     private final String mEpisodeNumber;
629     private final String mEpisodeTitle;
630     private final String mProgramDescription;
631     private final String mProgramLongDescription;
632     private final String mProgramPosterArtUri;
633     private final String mProgramThumbnailUri;
634     @RecordingState private final int mState;
635     private final long mSeriesRecordingId;
636     private final Long mRecordedProgramId;
637     private final Integer mFailedReason;
638     private final long mStartOffsetMs;
639     private final long mEndOffsetMs;
640 
ScheduledRecording( long id, long priority, String inputId, long channelId, long programId, String programTitle, @RecordingType int type, long startTime, long endTime, String seasonNumber, String episodeNumber, String episodeTitle, String programDescription, String programLongDescription, String programPosterArtUri, String programThumbnailUri, @RecordingState int state, long seriesRecordingId, Long recordedProgramId, Integer failedReason, long startOffsetMs, long endOffsetMs)641     private ScheduledRecording(
642             long id,
643             long priority,
644             String inputId,
645             long channelId,
646             long programId,
647             String programTitle,
648             @RecordingType int type,
649             long startTime,
650             long endTime,
651             String seasonNumber,
652             String episodeNumber,
653             String episodeTitle,
654             String programDescription,
655             String programLongDescription,
656             String programPosterArtUri,
657             String programThumbnailUri,
658             @RecordingState int state,
659             long seriesRecordingId,
660             Long recordedProgramId,
661             Integer failedReason,
662             long startOffsetMs,
663             long endOffsetMs) {
664         mId = id;
665         mPriority = priority;
666         mInputId = inputId;
667         mChannelId = channelId;
668         mProgramId = programId;
669         mProgramTitle = programTitle;
670         mType = type;
671         mStartTimeMs = startTime;
672         mEndTimeMs = endTime;
673         mSeasonNumber = seasonNumber;
674         mEpisodeNumber = episodeNumber;
675         mEpisodeTitle = episodeTitle;
676         mProgramDescription = programDescription;
677         mProgramLongDescription = programLongDescription;
678         mProgramPosterArtUri = programPosterArtUri;
679         mProgramThumbnailUri = programThumbnailUri;
680         mState = state;
681         mSeriesRecordingId = seriesRecordingId;
682         mRecordedProgramId = recordedProgramId;
683         mFailedReason = failedReason;
684         mStartOffsetMs = startOffsetMs;
685         mEndOffsetMs = endOffsetMs;
686     }
687 
688     /**
689      * Returns recording schedule type. The possible types are {@link #TYPE_PROGRAM} and {@link
690      * #TYPE_TIMED}.
691      */
692     @RecordingType
getType()693     public int getType() {
694         return mType;
695     }
696 
697     /** Returns schedules' input id. */
getInputId()698     public String getInputId() {
699         return mInputId;
700     }
701 
702     /** Returns recorded {@link Channel}. */
getChannelId()703     public long getChannelId() {
704         return mChannelId;
705     }
706 
707     /** Return the optional program id */
getProgramId()708     public long getProgramId() {
709         return mProgramId;
710     }
711 
712     /** Return the optional program Title */
getProgramTitle()713     public String getProgramTitle() {
714         return mProgramTitle;
715     }
716 
717     /** Returns started time. */
getStartTimeMs()718     public long getStartTimeMs() {
719         return mStartTimeMs;
720     }
721 
722     /** Returns ended time. */
getEndTimeMs()723     public long getEndTimeMs() {
724         return mEndTimeMs;
725     }
726 
727     /** Returns the season number. */
getSeasonNumber()728     public String getSeasonNumber() {
729         return mSeasonNumber;
730     }
731 
732     /** Returns the episode number. */
getEpisodeNumber()733     public String getEpisodeNumber() {
734         return mEpisodeNumber;
735     }
736 
737     /** Returns the episode title. */
getEpisodeTitle()738     public String getEpisodeTitle() {
739         return mEpisodeTitle;
740     }
741 
742     /** Returns the description of program. */
getProgramDescription()743     public String getProgramDescription() {
744         return mProgramDescription;
745     }
746 
747     /** Returns the long description of program. */
getProgramLongDescription()748     public String getProgramLongDescription() {
749         return mProgramLongDescription;
750     }
751 
752     /** Returns the poster uri of program. */
getProgramPosterArtUri()753     public String getProgramPosterArtUri() {
754         return mProgramPosterArtUri;
755     }
756 
757     /** Returns the thumb nail uri of program. */
getProgramThumbnailUri()758     public String getProgramThumbnailUri() {
759         return mProgramThumbnailUri;
760     }
761 
762     /** Returns duration. */
getDuration()763     public long getDuration() {
764         return mEndTimeMs - mStartTimeMs;
765     }
766 
767     /**
768      * Returns the state. The possible states are {@link #STATE_RECORDING_NOT_STARTED}, {@link
769      * #STATE_RECORDING_IN_PROGRESS}, {@link #STATE_RECORDING_FINISHED}, {@link
770      * #STATE_RECORDING_FAILED}, {@link #STATE_RECORDING_CLIPPED} and {@link
771      * #STATE_RECORDING_DELETED}.
772      */
773     @RecordingState
getState()774     public int getState() {
775         return mState;
776     }
777 
778     /** Returns the ID of the {@link SeriesRecording} including this schedule. */
getSeriesRecordingId()779     public long getSeriesRecordingId() {
780         return mSeriesRecordingId;
781     }
782 
783     /** Returns the ID of the corresponding {@link RecordedProgram}. */
784     @Nullable
getRecordedProgramId()785     public Long getRecordedProgramId() {
786         return mRecordedProgramId;
787     }
788 
789     /** Returns the failed reason of the {@link ScheduledRecording}. */
790     @Nullable
791     @RecordingFailedReason
getFailedReason()792     public Integer getFailedReason() {
793         return mFailedReason;
794     }
795 
796     /** Returns the start time offset. */
getStartOffsetMs()797     public long getStartOffsetMs() {
798         return mStartOffsetMs;
799     }
800 
801     /** Returns the end time offset. */
getEndOffsetMs()802     public long getEndOffsetMs() {
803         return mEndOffsetMs;
804     }
805 
getId()806     public long getId() {
807         return mId;
808     }
809 
810     /** Sets the ID; */
setId(long id)811     public void setId(long id) {
812         mId = id;
813     }
814 
getPriority()815     public long getPriority() {
816         return mPriority;
817     }
818 
819     /** Returns season number, episode number and episode title for display. */
getEpisodeDisplayTitle(Context context)820     public String getEpisodeDisplayTitle(Context context) {
821         if (!TextUtils.isEmpty(mEpisodeNumber)) {
822             String episodeTitle = mEpisodeTitle == null ? "" : mEpisodeTitle;
823             if (TextUtils.equals(mSeasonNumber, "0")) {
824                 // Do not show "S0: ".
825                 return String.format(
826                         context.getResources()
827                                 .getString(R.string.display_episode_title_format_no_season_number),
828                         mEpisodeNumber,
829                         episodeTitle);
830             } else {
831                 return String.format(
832                         context.getResources().getString(R.string.display_episode_title_format),
833                         mSeasonNumber,
834                         mEpisodeNumber,
835                         episodeTitle);
836             }
837         }
838         return mEpisodeTitle;
839     }
840 
841     /**
842      * Returns the program's display title, if the program title is not null, returns program title.
843      * Otherwise returns the channel name.
844      */
getProgramDisplayTitle(Context context)845     public String getProgramDisplayTitle(Context context) {
846         if (!TextUtils.isEmpty(mProgramTitle)) {
847             return mProgramTitle;
848         }
849         Channel channel =
850                 TvSingletons.getSingletons(context).getChannelDataManager().getChannel(mChannelId);
851         return channel != null
852                 ? channel.getDisplayName()
853                 : context.getString(R.string.no_program_information);
854     }
855 
856     /** Converts a string to a @RecordingType int, defaulting to {@link #TYPE_TIMED}. */
recordingType(String type)857     private static @RecordingType int recordingType(String type) {
858         switch (type) {
859             case Schedules.TYPE_TIMED:
860                 return TYPE_TIMED;
861             case Schedules.TYPE_PROGRAM:
862                 return TYPE_PROGRAM;
863             default:
864                 SoftPreconditions.checkArgument(false, TAG, "Unknown recording type %s", type);
865                 return TYPE_TIMED;
866         }
867     }
868 
869     /** Converts a @RecordingType int to a string, defaulting to {@link Schedules#TYPE_TIMED}. */
recordingType(@ecordingType int type)870     private static String recordingType(@RecordingType int type) {
871         switch (type) {
872             case TYPE_TIMED:
873                 return Schedules.TYPE_TIMED;
874             case TYPE_PROGRAM:
875                 return Schedules.TYPE_PROGRAM;
876             default:
877                 SoftPreconditions.checkArgument(false, TAG, "Unknown recording type %s", type);
878                 return Schedules.TYPE_TIMED;
879         }
880     }
881 
882     /**
883      * Converts a string to a @RecordingState int, defaulting to {@link
884      * #STATE_RECORDING_NOT_STARTED}.
885      */
recordingState(String state)886     private static @RecordingState int recordingState(String state) {
887         switch (state) {
888             case Schedules.STATE_RECORDING_NOT_STARTED:
889                 return STATE_RECORDING_NOT_STARTED;
890             case Schedules.STATE_RECORDING_IN_PROGRESS:
891                 return STATE_RECORDING_IN_PROGRESS;
892             case Schedules.STATE_RECORDING_FINISHED:
893                 return STATE_RECORDING_FINISHED;
894             case Schedules.STATE_RECORDING_FAILED:
895                 return STATE_RECORDING_FAILED;
896             case Schedules.STATE_RECORDING_CLIPPED:
897                 return STATE_RECORDING_CLIPPED;
898             case Schedules.STATE_RECORDING_DELETED:
899                 return STATE_RECORDING_DELETED;
900             case Schedules.STATE_RECORDING_CANCELED:
901                 return STATE_RECORDING_CANCELED;
902             default:
903                 SoftPreconditions.checkArgument(false, TAG, "Unknown recording state %s", state);
904                 return STATE_RECORDING_NOT_STARTED;
905         }
906     }
907 
908     /**
909      * Converts a @RecordingState int to string, defaulting to {@link
910      * Schedules#STATE_RECORDING_NOT_STARTED}.
911      */
recordingState(@ecordingState int state)912     private static String recordingState(@RecordingState int state) {
913         switch (state) {
914             case STATE_RECORDING_NOT_STARTED:
915                 return Schedules.STATE_RECORDING_NOT_STARTED;
916             case STATE_RECORDING_IN_PROGRESS:
917                 return Schedules.STATE_RECORDING_IN_PROGRESS;
918             case STATE_RECORDING_FINISHED:
919                 return Schedules.STATE_RECORDING_FINISHED;
920             case STATE_RECORDING_FAILED:
921                 return Schedules.STATE_RECORDING_FAILED;
922             case STATE_RECORDING_CLIPPED:
923                 return Schedules.STATE_RECORDING_CLIPPED;
924             case STATE_RECORDING_DELETED:
925                 return Schedules.STATE_RECORDING_DELETED;
926             case STATE_RECORDING_CANCELED:
927                 return Schedules.STATE_RECORDING_CANCELED;
928             default:
929                 SoftPreconditions.checkArgument(false, TAG, "Unknown recording state %s", state);
930                 return Schedules.STATE_RECORDING_NOT_STARTED;
931         }
932     }
933 
934     /** Converts a string to a failed reason integer, defaulting to {@link #FAILED_REASON_OTHER}. */
recordingFailedReason(String reason)935     private static Integer recordingFailedReason(String reason) {
936         if (TextUtils.isEmpty(reason)) {
937             return null;
938         }
939         switch (reason) {
940             case Schedules.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
941                 return FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
942             case Schedules.FAILED_REASON_NOT_FINISHED:
943                 return FAILED_REASON_NOT_FINISHED;
944             case Schedules.FAILED_REASON_SCHEDULER_STOPPED:
945                 return FAILED_REASON_SCHEDULER_STOPPED;
946             case Schedules.FAILED_REASON_INVALID_CHANNEL:
947                 return FAILED_REASON_INVALID_CHANNEL;
948             case Schedules.FAILED_REASON_MESSAGE_NOT_SENT:
949                 return FAILED_REASON_MESSAGE_NOT_SENT;
950             case Schedules.FAILED_REASON_CONNECTION_FAILED:
951                 return FAILED_REASON_CONNECTION_FAILED;
952             case Schedules.FAILED_REASON_RESOURCE_BUSY:
953                 return FAILED_REASON_RESOURCE_BUSY;
954             case Schedules.FAILED_REASON_INPUT_UNAVAILABLE:
955                 return FAILED_REASON_INPUT_UNAVAILABLE;
956             case Schedules.FAILED_REASON_INPUT_DVR_UNSUPPORTED:
957                 return FAILED_REASON_INPUT_DVR_UNSUPPORTED;
958             case Schedules.FAILED_REASON_INSUFFICIENT_SPACE:
959                 return FAILED_REASON_INSUFFICIENT_SPACE;
960             case Schedules.FAILED_REASON_OTHER:
961             default:
962                 return FAILED_REASON_OTHER;
963         }
964     }
965 
966     /**
967      * Converts a failed reason integer to string, defaulting to {@link
968      * Schedules#FAILED_REASON_OTHER}.
969      */
recordingFailedReason(Integer reason)970     private static String recordingFailedReason(Integer reason) {
971         if (reason == null) {
972             return null;
973         }
974         switch (reason) {
975             case FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
976                 return Schedules.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
977             case FAILED_REASON_NOT_FINISHED:
978                 return Schedules.FAILED_REASON_NOT_FINISHED;
979             case FAILED_REASON_SCHEDULER_STOPPED:
980                 return Schedules.FAILED_REASON_SCHEDULER_STOPPED;
981             case FAILED_REASON_INVALID_CHANNEL:
982                 return Schedules.FAILED_REASON_INVALID_CHANNEL;
983             case FAILED_REASON_MESSAGE_NOT_SENT:
984                 return Schedules.FAILED_REASON_MESSAGE_NOT_SENT;
985             case FAILED_REASON_CONNECTION_FAILED:
986                 return Schedules.FAILED_REASON_CONNECTION_FAILED;
987             case FAILED_REASON_RESOURCE_BUSY:
988                 return Schedules.FAILED_REASON_RESOURCE_BUSY;
989             case FAILED_REASON_INPUT_UNAVAILABLE:
990                 return Schedules.FAILED_REASON_INPUT_UNAVAILABLE;
991             case FAILED_REASON_INPUT_DVR_UNSUPPORTED:
992                 return Schedules.FAILED_REASON_INPUT_DVR_UNSUPPORTED;
993             case FAILED_REASON_INSUFFICIENT_SPACE:
994                 return Schedules.FAILED_REASON_INSUFFICIENT_SPACE;
995             case FAILED_REASON_OTHER: // fall through
996             default:
997                 return Schedules.FAILED_REASON_OTHER;
998         }
999     }
1000 
1001     /** Checks if the {@code period} overlaps with the recording time. */
isOverLapping(Range<Long> period)1002     public boolean isOverLapping(Range<Long> period) {
1003         return mStartTimeMs < period.getUpper() && mEndTimeMs > period.getLower();
1004     }
1005 
1006     /** Checks if the {@code schedule} overlaps with this schedule. */
isOverLapping(ScheduledRecording schedule)1007     public boolean isOverLapping(ScheduledRecording schedule) {
1008         return mStartTimeMs < schedule.getEndTimeMs() && mEndTimeMs > schedule.getStartTimeMs();
1009     }
1010 
1011     @Override
toString()1012     public String toString() {
1013         return "ScheduledRecording["
1014                 + mId
1015                 + "]"
1016                 + "(inputId="
1017                 + mInputId
1018                 + ",channelId="
1019                 + mChannelId
1020                 + ",programId="
1021                 + mProgramId
1022                 + ",programTitle="
1023                 + mProgramTitle
1024                 + ",type="
1025                 + mType
1026                 + ",startTime="
1027                 + CommonUtils.toIsoDateTimeString(mStartTimeMs)
1028                 + "("
1029                 + mStartTimeMs
1030                 + ")"
1031                 + ",endTime="
1032                 + CommonUtils.toIsoDateTimeString(mEndTimeMs)
1033                 + "("
1034                 + mEndTimeMs
1035                 + ")"
1036                 + ",seasonNumber="
1037                 + mSeasonNumber
1038                 + ",episodeNumber="
1039                 + mEpisodeNumber
1040                 + ",episodeTitle="
1041                 + mEpisodeTitle
1042                 + ",programDescription="
1043                 + mProgramDescription
1044                 + ",programLongDescription="
1045                 + mProgramLongDescription
1046                 + ",programPosterArtUri="
1047                 + mProgramPosterArtUri
1048                 + ",programThumbnailUri="
1049                 + mProgramThumbnailUri
1050                 + ",state="
1051                 + mState
1052                 + ",failedReason="
1053                 + mFailedReason
1054                 + ",priority="
1055                 + mPriority
1056                 + ",seriesRecordingId="
1057                 + mSeriesRecordingId
1058                 + ",startOffsetMs="
1059                 + mStartOffsetMs
1060                 + ",endOffsetMs="
1061                 + mEndOffsetMs
1062                 + ")";
1063     }
1064 
1065     @Override
describeContents()1066     public int describeContents() {
1067         return 0;
1068     }
1069 
1070     @Override
writeToParcel(Parcel out, int paramInt)1071     public void writeToParcel(Parcel out, int paramInt) {
1072         out.writeLong(mId);
1073         out.writeLong(mPriority);
1074         out.writeString(mInputId);
1075         out.writeLong(mChannelId);
1076         out.writeLong(mProgramId);
1077         out.writeString(mProgramTitle);
1078         out.writeInt(mType);
1079         out.writeLong(mStartTimeMs);
1080         out.writeLong(mEndTimeMs);
1081         out.writeString(mSeasonNumber);
1082         out.writeString(mEpisodeNumber);
1083         out.writeString(mEpisodeTitle);
1084         out.writeString(mProgramDescription);
1085         out.writeString(mProgramLongDescription);
1086         out.writeString(mProgramPosterArtUri);
1087         out.writeString(mProgramThumbnailUri);
1088         out.writeInt(mState);
1089         out.writeString(recordingFailedReason(mFailedReason));
1090         out.writeLong(mSeriesRecordingId);
1091         out.writeLong(mStartOffsetMs);
1092         out.writeLong(mEndOffsetMs);
1093     }
1094 
1095     /** Returns {@code true} if the recording is not started yet, otherwise @{code false}. */
isNotStarted()1096     public boolean isNotStarted() {
1097         return mState == STATE_RECORDING_NOT_STARTED;
1098     }
1099 
1100     /** Returns {@code true} if the recording is in progress, otherwise @{code false}. */
isInProgress()1101     public boolean isInProgress() {
1102         return mState == STATE_RECORDING_IN_PROGRESS;
1103     }
1104 
1105     /** Returns {@code true} if the recording is finished, otherwise @{code false}. */
isFinished()1106     public boolean isFinished() {
1107         return mState == STATE_RECORDING_FINISHED;
1108     }
1109 
1110     /** Returns {@code true} if the recording is failed, otherwise @{code false}. */
isFailed()1111     public boolean isFailed() {
1112         return mState == STATE_RECORDING_FAILED;
1113     }
1114 
1115     @Override
equals(Object obj)1116     public boolean equals(Object obj) {
1117         if (!(obj instanceof ScheduledRecording)) {
1118             return false;
1119         }
1120         ScheduledRecording r = (ScheduledRecording) obj;
1121         return mId == r.mId
1122                 && mPriority == r.mPriority
1123                 && mChannelId == r.mChannelId
1124                 && mProgramId == r.mProgramId
1125                 && Objects.equals(mProgramTitle, r.mProgramTitle)
1126                 && mType == r.mType
1127                 && mStartTimeMs == r.mStartTimeMs
1128                 && mEndTimeMs == r.mEndTimeMs
1129                 && Objects.equals(mSeasonNumber, r.mSeasonNumber)
1130                 && Objects.equals(mEpisodeNumber, r.mEpisodeNumber)
1131                 && Objects.equals(mEpisodeTitle, r.mEpisodeTitle)
1132                 && Objects.equals(mProgramDescription, r.getProgramDescription())
1133                 && Objects.equals(mProgramLongDescription, r.getProgramLongDescription())
1134                 && Objects.equals(mProgramPosterArtUri, r.getProgramPosterArtUri())
1135                 && Objects.equals(mProgramThumbnailUri, r.getProgramThumbnailUri())
1136                 && mState == r.mState
1137                 && Objects.equals(mFailedReason, r.mFailedReason)
1138                 && mSeriesRecordingId == r.mSeriesRecordingId
1139                 && mStartOffsetMs == r.mStartOffsetMs
1140                 && mEndOffsetMs == r.mEndOffsetMs;
1141     }
1142 
1143     @Override
hashCode()1144     public int hashCode() {
1145         return Objects.hash(
1146                 mId,
1147                 mPriority,
1148                 mChannelId,
1149                 mProgramId,
1150                 mProgramTitle,
1151                 mType,
1152                 mStartTimeMs,
1153                 mEndTimeMs,
1154                 mSeasonNumber,
1155                 mEpisodeNumber,
1156                 mEpisodeTitle,
1157                 mProgramDescription,
1158                 mProgramLongDescription,
1159                 mProgramPosterArtUri,
1160                 mProgramThumbnailUri,
1161                 mState,
1162                 mFailedReason,
1163                 mSeriesRecordingId,
1164                 mStartOffsetMs,
1165                 mEndOffsetMs);
1166     }
1167 
1168     /** Returns an array containing all of the elements in the list. */
toArray(Collection<ScheduledRecording> schedules)1169     public static ScheduledRecording[] toArray(Collection<ScheduledRecording> schedules) {
1170         return schedules.toArray(new ScheduledRecording[schedules.size()]);
1171     }
1172 }
1173