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.data; 18 19 import android.content.Context; 20 import android.database.Cursor; 21 import android.media.tv.TvContentRating; 22 import android.media.tv.TvContract; 23 import android.support.annotation.NonNull; 24 import android.support.annotation.UiThread; 25 import android.support.v4.os.BuildCompat; 26 import android.text.TextUtils; 27 import android.util.Log; 28 29 import com.android.tv.R; 30 import com.android.tv.common.BuildConfig; 31 import com.android.tv.common.CollectionUtils; 32 import com.android.tv.common.TvContentRatingCache; 33 import com.android.tv.util.ImageLoader; 34 import com.android.tv.util.Utils; 35 36 import java.util.Arrays; 37 import java.util.Objects; 38 39 /** 40 * A convenience class to create and insert program information entries into the database. 41 */ 42 public final class Program implements Comparable<Program> { 43 private static final boolean DEBUG = false; 44 private static final boolean DEBUG_DUMP_DESCRIPTION = false; 45 private static final String TAG = "Program"; 46 47 private static final String[] PROJECTION_BASE = { 48 // Columns must match what is read in Program.fromCursor() 49 TvContract.Programs._ID, 50 TvContract.Programs.COLUMN_CHANNEL_ID, 51 TvContract.Programs.COLUMN_TITLE, 52 TvContract.Programs.COLUMN_EPISODE_TITLE, 53 TvContract.Programs.COLUMN_SHORT_DESCRIPTION, 54 TvContract.Programs.COLUMN_POSTER_ART_URI, 55 TvContract.Programs.COLUMN_THUMBNAIL_URI, 56 TvContract.Programs.COLUMN_CANONICAL_GENRE, 57 TvContract.Programs.COLUMN_CONTENT_RATING, 58 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 59 TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, 60 TvContract.Programs.COLUMN_VIDEO_WIDTH, 61 TvContract.Programs.COLUMN_VIDEO_HEIGHT 62 }; 63 64 // Columns which is deprecated in NYC 65 private static final String[] PROJECTION_DEPRECATED_IN_NYC = { 66 TvContract.Programs.COLUMN_SEASON_NUMBER, 67 TvContract.Programs.COLUMN_EPISODE_NUMBER 68 }; 69 70 private static final String[] PROJECTION_ADDED_IN_NYC = { 71 TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, 72 TvContract.Programs.COLUMN_SEASON_TITLE, 73 TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER 74 }; 75 76 public static final String[] PROJECTION = createProjection(); 77 78 private static String[] createProjection() { 79 return CollectionUtils 80 .concatAll(PROJECTION_BASE, BuildCompat.isAtLeastN() ? PROJECTION_ADDED_IN_NYC 81 : PROJECTION_DEPRECATED_IN_NYC); 82 } 83 84 /** 85 * Creates {@code Program} object from cursor. 86 * 87 * <p>The query that created the cursor MUST use {@link #PROJECTION}. 88 */ 89 public static Program fromCursor(Cursor cursor) { 90 // Columns read must match the order of match {@link #PROJECTION} 91 Builder builder = new Builder(); 92 int index = 0; 93 builder.setId(cursor.getLong(index++)); 94 builder.setChannelId(cursor.getLong(index++)); 95 builder.setTitle(cursor.getString(index++)); 96 builder.setEpisodeTitle(cursor.getString(index++)); 97 builder.setDescription(cursor.getString(index++)); 98 builder.setPosterArtUri(cursor.getString(index++)); 99 builder.setThumbnailUri(cursor.getString(index++)); 100 builder.setCanonicalGenres(cursor.getString(index++)); 101 builder.setContentRatings( 102 TvContentRatingCache.getInstance().getRatings(cursor.getString(index++))); 103 builder.setStartTimeUtcMillis(cursor.getLong(index++)); 104 builder.setEndTimeUtcMillis(cursor.getLong(index++)); 105 builder.setVideoWidth((int) cursor.getLong(index++)); 106 builder.setVideoHeight((int) cursor.getLong(index++)); 107 if (BuildCompat.isAtLeastN()) { 108 builder.setSeasonNumber(cursor.getString(index++)); 109 builder.setSeasonTitle(cursor.getString(index++)); 110 builder.setEpisodeNumber(cursor.getString(index++)); 111 } else { 112 builder.setSeasonNumber(cursor.getString(index++)); 113 builder.setEpisodeNumber(cursor.getString(index++)); 114 } 115 return builder.build(); 116 } 117 118 private long mId; 119 private long mChannelId; 120 private String mTitle; 121 private String mEpisodeTitle; 122 private String mSeasonNumber; 123 private String mSeasonTitle; 124 private String mEpisodeNumber; 125 private long mStartTimeUtcMillis; 126 private long mEndTimeUtcMillis; 127 private String mDescription; 128 private int mVideoWidth; 129 private int mVideoHeight; 130 private String mPosterArtUri; 131 private String mThumbnailUri; 132 private int[] mCanonicalGenreIds; 133 private TvContentRating[] mContentRatings; 134 135 /** 136 * TODO(DVR): Need to fill the following data. 137 */ 138 private boolean mRecordable; 139 private boolean mRecordingScheduled; 140 141 private Program() { 142 // Do nothing. 143 } 144 145 public long getId() { 146 return mId; 147 } 148 149 public long getChannelId() { 150 return mChannelId; 151 } 152 153 /** 154 * Returns {@code true} if this program is valid or {@code false} otherwise. 155 */ 156 public boolean isValid() { 157 return mChannelId >= 0; 158 } 159 160 /** 161 * Returns {@code true} if the program is valid and {@code false} otherwise. 162 */ 163 public static boolean isValid(Program program) { 164 return program != null && program.isValid(); 165 } 166 167 public String getTitle() { 168 return mTitle; 169 } 170 171 public String getEpisodeTitle() { 172 return mEpisodeTitle; 173 } 174 175 public String getEpisodeDisplayTitle(Context context) { 176 if (!TextUtils.isEmpty(mSeasonNumber) && !TextUtils.isEmpty(mEpisodeNumber) 177 && !TextUtils.isEmpty(mEpisodeTitle)) { 178 return String.format(context.getResources().getString(R.string.episode_format), 179 mSeasonNumber, mEpisodeNumber, mEpisodeTitle); 180 } 181 return mEpisodeTitle; 182 } 183 184 public String getSeasonNumber() { 185 return mSeasonNumber; 186 } 187 188 public String getEpisodeNumber() { 189 return mEpisodeNumber; 190 } 191 192 public long getStartTimeUtcMillis() { 193 return mStartTimeUtcMillis; 194 } 195 196 public long getEndTimeUtcMillis() { 197 return mEndTimeUtcMillis; 198 } 199 200 /** 201 * Returns the program duration. 202 */ 203 public long getDurationMillis() { 204 return mEndTimeUtcMillis - mStartTimeUtcMillis; 205 } 206 207 public String getDescription() { 208 return mDescription; 209 } 210 211 public int getVideoWidth() { 212 return mVideoWidth; 213 } 214 215 public int getVideoHeight() { 216 return mVideoHeight; 217 } 218 219 public TvContentRating[] getContentRatings() { 220 return mContentRatings; 221 } 222 223 public String getPosterArtUri() { 224 return mPosterArtUri; 225 } 226 227 public String getThumbnailUri() { 228 return mThumbnailUri; 229 } 230 231 /** 232 * Returns array of canonical genres for this program. 233 * This is expected to be called rarely. 234 */ 235 public String[] getCanonicalGenres() { 236 if (mCanonicalGenreIds == null) { 237 return null; 238 } 239 String[] genres = new String[mCanonicalGenreIds.length]; 240 for (int i = 0; i < mCanonicalGenreIds.length; i++) { 241 genres[i] = GenreItems.getCanonicalGenre(mCanonicalGenreIds[i]); 242 } 243 return genres; 244 } 245 246 /** 247 * Returns if this program has the genre. 248 */ 249 public boolean hasGenre(int genreId) { 250 if (genreId == GenreItems.ID_ALL_CHANNELS) { 251 return true; 252 } 253 if (mCanonicalGenreIds != null) { 254 for (int id : mCanonicalGenreIds) { 255 if (id == genreId) { 256 return true; 257 } 258 } 259 } 260 return false; 261 } 262 263 @Override 264 public int hashCode() { 265 return Objects.hash(mChannelId, mStartTimeUtcMillis, mEndTimeUtcMillis, 266 mTitle, mEpisodeTitle, mDescription, mVideoWidth, mVideoHeight, 267 mPosterArtUri, mThumbnailUri, Arrays.hashCode(mContentRatings), 268 Arrays.hashCode(mCanonicalGenreIds), mSeasonNumber, mSeasonTitle, mEpisodeNumber); 269 } 270 271 @Override 272 public boolean equals(Object other) { 273 if (!(other instanceof Program)) { 274 return false; 275 } 276 Program program = (Program) other; 277 return mChannelId == program.mChannelId 278 && mStartTimeUtcMillis == program.mStartTimeUtcMillis 279 && mEndTimeUtcMillis == program.mEndTimeUtcMillis 280 && Objects.equals(mTitle, program.mTitle) 281 && Objects.equals(mEpisodeTitle, program.mEpisodeTitle) 282 && Objects.equals(mDescription, program.mDescription) 283 && mVideoWidth == program.mVideoWidth 284 && mVideoHeight == program.mVideoHeight 285 && Objects.equals(mPosterArtUri, program.mPosterArtUri) 286 && Objects.equals(mThumbnailUri, program.mThumbnailUri) 287 && Arrays.equals(mContentRatings, program.mContentRatings) 288 && Arrays.equals(mCanonicalGenreIds, program.mCanonicalGenreIds) 289 && Objects.equals(mSeasonNumber, program.mSeasonNumber) 290 && Objects.equals(mSeasonTitle, program.mSeasonTitle) 291 && Objects.equals(mEpisodeNumber, program.mEpisodeNumber); 292 } 293 294 @Override 295 public int compareTo(@NonNull Program other) { 296 return Long.compare(mStartTimeUtcMillis, other.mStartTimeUtcMillis); 297 } 298 299 @Override 300 public String toString() { 301 StringBuilder builder = new StringBuilder(); 302 builder.append("Program[" + mId + "]{") 303 .append("channelId=").append(mChannelId) 304 .append(", title=").append(mTitle) 305 .append(", episodeTitle=").append(mEpisodeTitle) 306 .append(", seasonNumber=").append(mSeasonNumber) 307 .append(", seasonTitle=").append(mSeasonTitle) 308 .append(", episodeNumber=").append(mEpisodeNumber) 309 .append(", startTimeUtcSec=").append(Utils.toTimeString(mStartTimeUtcMillis)) 310 .append(", endTimeUtcSec=").append(Utils.toTimeString(mEndTimeUtcMillis)) 311 .append(", videoWidth=").append(mVideoWidth) 312 .append(", videoHeight=").append(mVideoHeight) 313 .append(", contentRatings=") 314 .append(TvContentRatingCache.contentRatingsToString(mContentRatings)) 315 .append(", posterArtUri=").append(mPosterArtUri) 316 .append(", thumbnailUri=").append(mThumbnailUri) 317 .append(", canonicalGenres=").append(Arrays.toString(mCanonicalGenreIds)); 318 if (DEBUG_DUMP_DESCRIPTION) { 319 builder.append(", description=").append(mDescription); 320 } 321 return builder.append("}").toString(); 322 } 323 324 public void copyFrom(Program other) { 325 if (this == other) { 326 return; 327 } 328 329 mId = other.mId; 330 mChannelId = other.mChannelId; 331 mTitle = other.mTitle; 332 mEpisodeTitle = other.mEpisodeTitle; 333 mSeasonNumber = other.mSeasonNumber; 334 mSeasonTitle = other.mSeasonTitle; 335 mEpisodeNumber = other.mEpisodeNumber; 336 mStartTimeUtcMillis = other.mStartTimeUtcMillis; 337 mEndTimeUtcMillis = other.mEndTimeUtcMillis; 338 mDescription = other.mDescription; 339 mVideoWidth = other.mVideoWidth; 340 mVideoHeight = other.mVideoHeight; 341 mPosterArtUri = other.mPosterArtUri; 342 mThumbnailUri = other.mThumbnailUri; 343 mCanonicalGenreIds = other.mCanonicalGenreIds; 344 mContentRatings = other.mContentRatings; 345 } 346 347 public static final class Builder { 348 private final Program mProgram; 349 private long mId; 350 351 public Builder() { 352 mProgram = new Program(); 353 // Fill initial data. 354 mProgram.mChannelId = Channel.INVALID_ID; 355 mProgram.mTitle = null; 356 mProgram.mSeasonNumber = null; 357 mProgram.mSeasonTitle = null; 358 mProgram.mEpisodeNumber = null; 359 mProgram.mStartTimeUtcMillis = -1; 360 mProgram.mEndTimeUtcMillis = -1; 361 mProgram.mDescription = null; 362 } 363 364 public Builder(Program other) { 365 mProgram = new Program(); 366 mProgram.copyFrom(other); 367 } 368 369 public Builder setId(long id) { 370 mProgram.mId = id; 371 return this; 372 } 373 374 public Builder setChannelId(long channelId) { 375 mProgram.mChannelId = channelId; 376 return this; 377 } 378 379 public Builder setTitle(String title) { 380 mProgram.mTitle = title; 381 return this; 382 } 383 384 public Builder setEpisodeTitle(String episodeTitle) { 385 mProgram.mEpisodeTitle = episodeTitle; 386 return this; 387 } 388 389 public Builder setSeasonNumber(String seasonNumber) { 390 mProgram.mSeasonNumber = seasonNumber; 391 return this; 392 } 393 394 public Builder setSeasonTitle(String seasonTitle) { 395 mProgram.mSeasonTitle = seasonTitle; 396 return this; 397 } 398 399 public Builder setEpisodeNumber(String episodeNumber) { 400 mProgram.mEpisodeNumber = episodeNumber; 401 return this; 402 } 403 404 public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { 405 mProgram.mStartTimeUtcMillis = startTimeUtcMillis; 406 return this; 407 } 408 409 public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { 410 mProgram.mEndTimeUtcMillis = endTimeUtcMillis; 411 return this; 412 } 413 414 public Builder setDescription(String description) { 415 mProgram.mDescription = description; 416 return this; 417 } 418 419 public Builder setVideoWidth(int width) { 420 mProgram.mVideoWidth = width; 421 return this; 422 } 423 424 public Builder setVideoHeight(int height) { 425 mProgram.mVideoHeight = height; 426 return this; 427 } 428 429 public Builder setContentRatings(TvContentRating[] contentRatings) { 430 mProgram.mContentRatings = contentRatings; 431 return this; 432 } 433 434 public Builder setPosterArtUri(String posterArtUri) { 435 mProgram.mPosterArtUri = posterArtUri; 436 return this; 437 } 438 439 public Builder setThumbnailUri(String thumbnailUri) { 440 mProgram.mThumbnailUri = thumbnailUri; 441 return this; 442 } 443 444 public Builder setCanonicalGenres(String genres) { 445 if (TextUtils.isEmpty(genres)) { 446 return this; 447 } 448 String[] canonicalGenres = TvContract.Programs.Genres.decode(genres); 449 if (canonicalGenres.length > 0) { 450 int[] temp = new int[canonicalGenres.length]; 451 int i = 0; 452 for (String canonicalGenre : canonicalGenres) { 453 int genreId = GenreItems.getId(canonicalGenre); 454 if (genreId == GenreItems.ID_ALL_CHANNELS) { 455 // Skip if the genre is unknown. 456 continue; 457 } 458 temp[i++] = genreId; 459 } 460 if (i < canonicalGenres.length) { 461 temp = Arrays.copyOf(temp, i); 462 } 463 mProgram.mCanonicalGenreIds=temp; 464 } 465 return this; 466 } 467 468 public Program build() { 469 Program program = new Program(); 470 program.copyFrom(mProgram); 471 return program; 472 } 473 } 474 475 /** 476 * Prefetches the program poster art.<p> 477 */ 478 public void prefetchPosterArt(Context context, int posterArtWidth, int posterArtHeight) { 479 if (mPosterArtUri == null) { 480 return; 481 } 482 ImageLoader.prefetchBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight); 483 } 484 485 /** 486 * Loads the program poster art and returns it via {@code callback}.<p> 487 * <p> 488 * Note that it may directly call {@code callback} if the program poster art already is loaded. 489 */ 490 @UiThread 491 public void loadPosterArt(Context context, int posterArtWidth, int posterArtHeight, 492 ImageLoader.ImageLoaderCallback callback) { 493 if (mPosterArtUri == null) { 494 return; 495 } 496 ImageLoader.loadBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight, callback); 497 } 498 499 public static boolean isDuplicate(Program p1, Program p2) { 500 if (p1 == null || p2 == null) { 501 return false; 502 } 503 boolean isDuplicate = p1.getChannelId() == p2.getChannelId() 504 && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis() 505 && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis(); 506 if (DEBUG && BuildConfig.ENG && isDuplicate) { 507 Log.w(TAG, "Duplicate programs detected! - \"" + p1.getTitle() + "\" and \"" 508 + p2.getTitle() + "\""); 509 } 510 return isDuplicate; 511 } 512 } 513