1 /*
2  * Copyright (C) 2016 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.ContentUris;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.media.tv.TvContentRating;
25 import android.media.tv.TvContract.Programs.Genres;
26 import android.media.tv.TvContract.RecordedPrograms;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.support.annotation.CheckResult;
30 import android.support.annotation.Nullable;
31 import android.support.annotation.WorkerThread;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import com.android.tv.common.R;
35 import com.android.tv.common.TvContentRatingCache;
36 import com.android.tv.common.data.RecordedProgramState;
37 import com.android.tv.common.util.CommonUtils;
38 import com.android.tv.common.util.StringUtils;
39 import com.android.tv.data.BaseProgramImpl;
40 import com.android.tv.data.GenreItems;
41 import com.android.tv.data.InternalDataUtils;
42 import com.android.tv.data.api.BaseProgram;
43 import com.android.tv.util.TvProviderUtils;
44 import com.google.auto.value.AutoValue;
45 import com.google.common.collect.ImmutableList;
46 import java.util.Collection;
47 import java.util.Comparator;
48 import java.util.concurrent.TimeUnit;
49 
50 /** Immutable instance of {@link android.media.tv.TvContract.RecordedPrograms}. */
51 @TargetApi(Build.VERSION_CODES.N)
52 @AutoValue
53 public abstract class RecordedProgram extends BaseProgramImpl {
54     public static final int ID_NOT_SET = -1;
55     private static final String TAG = "RecordedProgram";
56 
57     public static final String[] PROJECTION = {
58         RecordedPrograms._ID,
59         RecordedPrograms.COLUMN_PACKAGE_NAME,
60         RecordedPrograms.COLUMN_INPUT_ID,
61         RecordedPrograms.COLUMN_CHANNEL_ID,
62         RecordedPrograms.COLUMN_TITLE,
63         RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER,
64         RecordedPrograms.COLUMN_SEASON_TITLE,
65         RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER,
66         RecordedPrograms.COLUMN_EPISODE_TITLE,
67         RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
68         RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS,
69         RecordedPrograms.COLUMN_BROADCAST_GENRE,
70         RecordedPrograms.COLUMN_CANONICAL_GENRE,
71         RecordedPrograms.COLUMN_SHORT_DESCRIPTION,
72         RecordedPrograms.COLUMN_LONG_DESCRIPTION,
73         RecordedPrograms.COLUMN_VIDEO_WIDTH,
74         RecordedPrograms.COLUMN_VIDEO_HEIGHT,
75         RecordedPrograms.COLUMN_AUDIO_LANGUAGE,
76         RecordedPrograms.COLUMN_CONTENT_RATING,
77         RecordedPrograms.COLUMN_POSTER_ART_URI,
78         RecordedPrograms.COLUMN_THUMBNAIL_URI,
79         RecordedPrograms.COLUMN_SEARCHABLE,
80         RecordedPrograms.COLUMN_RECORDING_DATA_URI,
81         RecordedPrograms.COLUMN_RECORDING_DATA_BYTES,
82         RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
83         RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
84         RecordedPrograms.COLUMN_VERSION_NUMBER,
85         RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
86     };
87 
fromCursor(Cursor cursor)88     public static RecordedProgram fromCursor(Cursor cursor) {
89         int index = 0;
90         Builder builder =
91                 builder()
92                         .setId(cursor.getLong(index++))
93                         .setPackageName(cursor.getString(index++))
94                         .setInputId(cursor.getString(index++))
95                         .setChannelId(cursor.getLong(index++))
96                         .setTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
97                         .setSeasonNumber(StringUtils.nullToEmpty(cursor.getString(index++)))
98                         .setSeasonTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
99                         .setEpisodeNumber(StringUtils.nullToEmpty(cursor.getString(index++)))
100                         .setEpisodeTitle(StringUtils.nullToEmpty(cursor.getString(index++)))
101                         .setStartTimeUtcMillis(cursor.getLong(index++))
102                         .setEndTimeUtcMillis(cursor.getLong(index++))
103                         .setBroadcastGenres(cursor.getString(index++))
104                         .setCanonicalGenres(cursor.getString(index++))
105                         .setDescription(StringUtils.nullToEmpty(cursor.getString(index++)))
106                         .setLongDescription(StringUtils.nullToEmpty(cursor.getString(index++)))
107                         .setVideoWidth(cursor.getInt(index++))
108                         .setVideoHeight(cursor.getInt(index++))
109                         .setAudioLanguage(StringUtils.nullToEmpty(cursor.getString(index++)))
110                         .setContentRatings(
111                                 TvContentRatingCache.getInstance()
112                                         .getRatings(cursor.getString(index++)))
113                         .setPosterArtUri(StringUtils.nullToEmpty(cursor.getString(index++)))
114                         .setThumbnailUri(StringUtils.nullToEmpty(cursor.getString(index++)))
115                         .setSearchable(cursor.getInt(index++) == 1)
116                         .setDataUri(cursor.getString(index++))
117                         .setDataBytes(cursor.getLong(index++))
118                         .setDurationMillis(cursor.getLong(index++))
119                         .setExpireTimeUtcMillis(cursor.getLong(index++))
120                         .setVersionNumber(cursor.getInt(index++));
121         if (CommonUtils.isInBundledPackageSet(builder.getPackageName())) {
122             InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder);
123         }
124         index++;
125         if (TvProviderUtils.getRecordedProgramHasSeriesIdColumn()) {
126             builder.setSeriesId(StringUtils.nullToEmpty(cursor.getString(index++)));
127         }
128         if (TvProviderUtils.getRecordedProgramHasStateColumn()) {
129             builder.setState(cursor.getString(index++));
130         }
131         return builder.build();
132     }
133 
134     @WorkerThread
toValues(Context context, RecordedProgram recordedProgram)135     public static ContentValues toValues(Context context, RecordedProgram recordedProgram) {
136         ContentValues values = new ContentValues();
137         if (recordedProgram.getId() != ID_NOT_SET) {
138             values.put(RecordedPrograms._ID, recordedProgram.getId());
139         }
140         values.put(RecordedPrograms.COLUMN_INPUT_ID, recordedProgram.getInputId());
141         values.put(RecordedPrograms.COLUMN_CHANNEL_ID, recordedProgram.getChannelId());
142         values.put(RecordedPrograms.COLUMN_TITLE, recordedProgram.getTitle());
143         values.put(
144                 RecordedPrograms.COLUMN_SEASON_DISPLAY_NUMBER, recordedProgram.getSeasonNumber());
145         values.put(RecordedPrograms.COLUMN_SEASON_TITLE, recordedProgram.getSeasonTitle());
146         values.put(
147                 RecordedPrograms.COLUMN_EPISODE_DISPLAY_NUMBER, recordedProgram.getEpisodeNumber());
148         values.put(RecordedPrograms.COLUMN_EPISODE_TITLE, recordedProgram.getEpisodeTitle());
149         values.put(
150                 RecordedPrograms.COLUMN_START_TIME_UTC_MILLIS,
151                 recordedProgram.getStartTimeUtcMillis());
152         values.put(
153                 RecordedPrograms.COLUMN_END_TIME_UTC_MILLIS, recordedProgram.getEndTimeUtcMillis());
154         values.put(
155                 RecordedPrograms.COLUMN_BROADCAST_GENRE,
156                 safeEncode(recordedProgram.getBroadcastGenres()));
157         values.put(
158                 RecordedPrograms.COLUMN_CANONICAL_GENRE,
159                 safeEncode(recordedProgram.getCanonicalGenres()));
160         values.put(RecordedPrograms.COLUMN_SHORT_DESCRIPTION, recordedProgram.getDescription());
161         values.put(RecordedPrograms.COLUMN_LONG_DESCRIPTION, recordedProgram.getLongDescription());
162         if (recordedProgram.getVideoWidth() == 0) {
163             values.putNull(RecordedPrograms.COLUMN_VIDEO_WIDTH);
164         } else {
165             values.put(RecordedPrograms.COLUMN_VIDEO_WIDTH, recordedProgram.getVideoWidth());
166         }
167         if (recordedProgram.getVideoHeight() == 0) {
168             values.putNull(RecordedPrograms.COLUMN_VIDEO_HEIGHT);
169         } else {
170             values.put(RecordedPrograms.COLUMN_VIDEO_HEIGHT, recordedProgram.getVideoHeight());
171         }
172         values.put(RecordedPrograms.COLUMN_AUDIO_LANGUAGE, recordedProgram.getAudioLanguage());
173         values.put(
174                 RecordedPrograms.COLUMN_CONTENT_RATING,
175                 TvContentRatingCache.contentRatingsToString(recordedProgram.getContentRatings()));
176         values.put(RecordedPrograms.COLUMN_POSTER_ART_URI, recordedProgram.getPosterArtUri());
177         values.put(RecordedPrograms.COLUMN_THUMBNAIL_URI, recordedProgram.getThumbnailUri());
178         values.put(RecordedPrograms.COLUMN_SEARCHABLE, recordedProgram.isSearchable() ? 1 : 0);
179         values.put(
180                 RecordedPrograms.COLUMN_RECORDING_DATA_URI,
181                 safeToString(recordedProgram.getDataUri()));
182         values.put(RecordedPrograms.COLUMN_RECORDING_DATA_BYTES, recordedProgram.getDataBytes());
183         values.put(
184                 RecordedPrograms.COLUMN_RECORDING_DURATION_MILLIS,
185                 recordedProgram.getDurationMillis());
186         values.put(
187                 RecordedPrograms.COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS,
188                 recordedProgram.getExpireTimeUtcMillis());
189         values.put(
190                 RecordedPrograms.COLUMN_INTERNAL_PROVIDER_DATA,
191                 InternalDataUtils.serializeInternalProviderData(recordedProgram));
192         values.put(RecordedPrograms.COLUMN_VERSION_NUMBER, recordedProgram.getVersionNumber());
193         if (TvProviderUtils.checkSeriesIdColumn(context, RecordedPrograms.CONTENT_URI)) {
194             values.put(COLUMN_SERIES_ID, recordedProgram.getSeriesId());
195         }
196         if (TvProviderUtils.checkStateColumn(context, RecordedPrograms.CONTENT_URI)) {
197             values.put(COLUMN_STATE, recordedProgram.getState().toString());
198         }
199         return values;
200     }
201 
202     /** Builder for {@link RecordedProgram}s. */
203     @AutoValue.Builder
204     public abstract static class Builder {
205 
setId(long id)206         public abstract Builder setId(long id);
207 
setPackageName(String packageName)208         public abstract Builder setPackageName(String packageName);
209 
getPackageName()210         abstract String getPackageName();
211 
setInputId(String inputId)212         public abstract Builder setInputId(String inputId);
213 
setChannelId(long channelId)214         public abstract Builder setChannelId(long channelId);
215 
getTitle()216         abstract String getTitle();
217 
setTitle(String title)218         public abstract Builder setTitle(String title);
219 
getSeriesId()220         abstract String getSeriesId();
221 
setSeriesId(String seriesId)222         public abstract Builder setSeriesId(String seriesId);
223 
setSeasonNumber(String seasonNumber)224         public abstract Builder setSeasonNumber(String seasonNumber);
225 
setSeasonTitle(String seasonTitle)226         public abstract Builder setSeasonTitle(String seasonTitle);
227 
228         @Nullable
getEpisodeNumber()229         abstract String getEpisodeNumber();
230 
setEpisodeNumber(String episodeNumber)231         public abstract Builder setEpisodeNumber(String episodeNumber);
232 
setEpisodeTitle(String episodeTitle)233         public abstract Builder setEpisodeTitle(String episodeTitle);
234 
setStartTimeUtcMillis(long startTimeUtcMillis)235         public abstract Builder setStartTimeUtcMillis(long startTimeUtcMillis);
236 
setEndTimeUtcMillis(long endTimeUtcMillis)237         public abstract Builder setEndTimeUtcMillis(long endTimeUtcMillis);
238 
setState(RecordedProgramState state)239         public abstract Builder setState(RecordedProgramState state);
240 
setState(@ullable String state)241         public Builder setState(@Nullable String state) {
242 
243             if (!TextUtils.isEmpty(state)) {
244                 try {
245                     return setState(RecordedProgramState.valueOf(state));
246                 } catch (IllegalArgumentException e) {
247                     Log.w(TAG, "Unknown recording state " + state, e);
248                 }
249             }
250             return setState(RecordedProgramState.NOT_SET);
251         }
252 
setBroadcastGenres(@ullable String broadcastGenres)253         public Builder setBroadcastGenres(@Nullable String broadcastGenres) {
254             return setBroadcastGenres(
255                     TextUtils.isEmpty(broadcastGenres)
256                             ? ImmutableList.of()
257                             : ImmutableList.copyOf(Genres.decode(broadcastGenres)));
258         }
259 
setBroadcastGenres(ImmutableList<String> broadcastGenres)260         public abstract Builder setBroadcastGenres(ImmutableList<String> broadcastGenres);
261 
setCanonicalGenres(String canonicalGenres)262         public Builder setCanonicalGenres(String canonicalGenres) {
263             return setCanonicalGenres(
264                     TextUtils.isEmpty(canonicalGenres)
265                             ? ImmutableList.of()
266                             : ImmutableList.copyOf(Genres.decode(canonicalGenres)));
267         }
268 
setCanonicalGenres(ImmutableList<String> canonicalGenres)269         public abstract Builder setCanonicalGenres(ImmutableList<String> canonicalGenres);
270 
setDescription(String shortDescription)271         public abstract Builder setDescription(String shortDescription);
272 
setLongDescription(String longDescription)273         public abstract Builder setLongDescription(String longDescription);
274 
setVideoWidth(int videoWidth)275         public abstract Builder setVideoWidth(int videoWidth);
276 
setVideoHeight(int videoHeight)277         public abstract Builder setVideoHeight(int videoHeight);
278 
setAudioLanguage(String audioLanguage)279         public abstract Builder setAudioLanguage(String audioLanguage);
280 
setContentRatings(ImmutableList<TvContentRating> contentRatings)281         public abstract Builder setContentRatings(ImmutableList<TvContentRating> contentRatings);
282 
toUri(@ullable String uriString)283         private Uri toUri(@Nullable String uriString) {
284             try {
285                 return uriString == null ? null : Uri.parse(uriString);
286             } catch (Exception e) {
287                 return Uri.EMPTY;
288             }
289         }
290 
setPosterArtUri(String posterArtUri)291         public abstract Builder setPosterArtUri(String posterArtUri);
292 
setThumbnailUri(String thumbnailUri)293         public abstract Builder setThumbnailUri(String thumbnailUri);
294 
setSearchable(boolean searchable)295         public abstract Builder setSearchable(boolean searchable);
296 
setDataUri(@ullable String dataUri)297         public Builder setDataUri(@Nullable String dataUri) {
298             return setDataUri(toUri(dataUri));
299         }
300 
setDataUri(Uri dataUri)301         public abstract Builder setDataUri(Uri dataUri);
302 
setDataBytes(long dataBytes)303         public abstract Builder setDataBytes(long dataBytes);
304 
setDurationMillis(long durationMillis)305         public abstract Builder setDurationMillis(long durationMillis);
306 
setExpireTimeUtcMillis(long expireTimeUtcMillis)307         public abstract Builder setExpireTimeUtcMillis(long expireTimeUtcMillis);
308 
setVersionNumber(int versionNumber)309         public abstract Builder setVersionNumber(int versionNumber);
310 
autoBuild()311         abstract RecordedProgram autoBuild();
312 
build()313         public RecordedProgram build() {
314             if (TextUtils.isEmpty(getTitle())) {
315                 // If title is null, series cannot be generated for this program.
316                 setSeriesId(null);
317             } else if (TextUtils.isEmpty(getSeriesId()) && !TextUtils.isEmpty(getEpisodeNumber())) {
318                 // If series ID is not set, generate it for the episodic program of other TV input.
319                 setSeriesId(BaseProgram.generateSeriesId(getPackageName(), getTitle()));
320             }
321             return (autoBuild());
322         }
323     }
324 
builder()325     public static Builder builder() {
326         return new AutoValue_RecordedProgram.Builder()
327                 .setId(ID_NOT_SET)
328                 .setChannelId(ID_NOT_SET)
329                 .setAudioLanguage("")
330                 .setBroadcastGenres("")
331                 .setCanonicalGenres("")
332                 .setContentRatings(ImmutableList.of())
333                 .setDataUri("")
334                 .setDurationMillis(0)
335                 .setDescription("")
336                 .setDataBytes(0)
337                 .setLongDescription("")
338                 .setEndTimeUtcMillis(0)
339                 .setEpisodeNumber("")
340                 .setEpisodeTitle("")
341                 .setExpireTimeUtcMillis(0)
342                 .setPackageName("")
343                 .setPosterArtUri("")
344                 .setSeasonNumber("")
345                 .setSeasonTitle("")
346                 .setSearchable(false)
347                 .setSeriesId("")
348                 .setStartTimeUtcMillis(0)
349                 .setState(RecordedProgramState.NOT_SET)
350                 .setThumbnailUri("")
351                 .setTitle("")
352                 .setVersionNumber(0)
353                 .setVideoHeight(0)
354                 .setVideoWidth(0);
355     }
356 
357     public static final Comparator<RecordedProgram> START_TIME_THEN_ID_COMPARATOR =
358             (RecordedProgram lhs, RecordedProgram rhs) -> {
359                 int res = Long.compare(lhs.getStartTimeUtcMillis(), rhs.getStartTimeUtcMillis());
360                 if (res != 0) {
361                     return res;
362                 }
363                 return Long.compare(lhs.getId(), rhs.getId());
364             };
365 
366     private static final long CLIPPED_THRESHOLD_MS = TimeUnit.MINUTES.toMillis(5);
367 
getAudioLanguage()368     public abstract String getAudioLanguage();
369 
getBroadcastGenres()370     public abstract ImmutableList<String> getBroadcastGenres();
371 
getCanonicalGenres()372     public abstract ImmutableList<String> getCanonicalGenres();
373 
374     /** Returns array of canonical genre ID's for this recorded program. */
375     @Override
getCanonicalGenreIds()376     public int[] getCanonicalGenreIds() {
377 
378         ImmutableList<String> canonicalGenres = getCanonicalGenres();
379         int[] genreIds = new int[getCanonicalGenres().size()];
380         for (int i = 0; i < canonicalGenres.size(); i++) {
381             genreIds[i] = GenreItems.getId(canonicalGenres.get(i));
382         }
383         return genreIds;
384     }
385 
getDataUri()386     public abstract Uri getDataUri();
387 
getDataBytes()388     public abstract long getDataBytes();
389 
390     @Nullable
getEpisodeDisplayNumber(Context context)391     public String getEpisodeDisplayNumber(Context context) {
392         if (!TextUtils.isEmpty(getEpisodeNumber())) {
393             if (TextUtils.equals(getSeasonNumber(), "0")) {
394                 // Do not show "S0: ".
395                 return context.getResources()
396                         .getString(
397                                 R.string.display_episode_number_format_no_season_number,
398                                 getEpisodeNumber());
399             } else {
400                 return context.getResources()
401                         .getString(
402                                 R.string.display_episode_number_format,
403                                 getSeasonNumber(),
404                                 getEpisodeNumber());
405             }
406         }
407         return null;
408     }
409 
getExpireTimeUtcMillis()410     public abstract long getExpireTimeUtcMillis();
411 
getPackageName()412     public abstract String getPackageName();
413 
getInputId()414     public abstract String getInputId();
415 
416     @Override
isValid()417     public boolean isValid() {
418         return true;
419     }
420 
isVisible()421     public boolean isVisible() {
422         switch (getState()) {
423             case NOT_SET:
424             case FINISHED:
425                 return true;
426             default:
427                 return false;
428         }
429     }
430 
isPartial()431     public boolean isPartial() {
432         return getState() == RecordedProgramState.PARTIAL;
433     }
434 
isSearchable()435     public abstract boolean isSearchable();
436 
getSeasonTitle()437     public abstract String getSeasonTitle();
438 
getState()439     public abstract RecordedProgramState getState();
440 
getUri()441     public Uri getUri() {
442         return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, getId());
443     }
444 
getVersionNumber()445     public abstract int getVersionNumber();
446 
getVideoHeight()447     public abstract int getVideoHeight();
448 
getVideoWidth()449     public abstract int getVideoWidth();
450 
451     /** Checks whether the recording has been clipped or not. */
isClipped()452     public boolean isClipped() {
453         return getEndTimeUtcMillis() - getStartTimeUtcMillis() - getDurationMillis()
454                 > CLIPPED_THRESHOLD_MS;
455     }
456 
toBuilder()457     public abstract Builder toBuilder();
458 
459     @CheckResult
withId(long id)460     public RecordedProgram withId(long id) {
461         return toBuilder().setId(id).build();
462     }
463 
464     @Nullable
safeToString(@ullable Object o)465     private static String safeToString(@Nullable Object o) {
466         return o == null ? null : o.toString();
467     }
468 
469     @Nullable
safeEncode(@ullable ImmutableList<String> genres)470     private static String safeEncode(@Nullable ImmutableList<String> genres) {
471         return genres == null ? null : Genres.encode(genres.toArray(new String[0]));
472     }
473 
474     /** Returns an array containing all of the elements in the list. */
toArray(Collection<RecordedProgram> recordedPrograms)475     public static RecordedProgram[] toArray(Collection<RecordedProgram> recordedPrograms) {
476         return recordedPrograms.toArray(new RecordedProgram[recordedPrograms.size()]);
477     }
478 }
479