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