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