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.annotation.SuppressLint; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.media.tv.TvContentRating; 24 import android.media.tv.TvContract; 25 import android.media.tv.TvContract.Programs; 26 import android.os.Build; 27 import android.os.Parcel; 28 import android.os.Parcelable; 29 import android.support.annotation.NonNull; 30 import android.support.annotation.Nullable; 31 import android.support.annotation.UiThread; 32 import android.support.annotation.VisibleForTesting; 33 import android.text.TextUtils; 34 import android.util.Log; 35 36 import com.android.tv.common.BuildConfig; 37 import com.android.tv.common.CollectionUtils; 38 import com.android.tv.common.TvContentRatingCache; 39 import com.android.tv.util.ImageLoader; 40 import com.android.tv.util.Utils; 41 42 import java.io.Serializable; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.List; 46 import java.util.Objects; 47 48 /** 49 * A convenience class to create and insert program information entries into the database. 50 */ 51 public final class Program extends BaseProgram implements Comparable<Program>, Parcelable { 52 private static final boolean DEBUG = false; 53 private static final boolean DEBUG_DUMP_DESCRIPTION = false; 54 private static final String TAG = "Program"; 55 56 private static final String[] PROJECTION_BASE = { 57 // Columns must match what is read in Program.fromCursor() 58 TvContract.Programs._ID, 59 TvContract.Programs.COLUMN_PACKAGE_NAME, 60 TvContract.Programs.COLUMN_CHANNEL_ID, 61 TvContract.Programs.COLUMN_TITLE, 62 TvContract.Programs.COLUMN_EPISODE_TITLE, 63 TvContract.Programs.COLUMN_SHORT_DESCRIPTION, 64 TvContract.Programs.COLUMN_LONG_DESCRIPTION, 65 TvContract.Programs.COLUMN_POSTER_ART_URI, 66 TvContract.Programs.COLUMN_THUMBNAIL_URI, 67 TvContract.Programs.COLUMN_CANONICAL_GENRE, 68 TvContract.Programs.COLUMN_CONTENT_RATING, 69 TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 70 TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, 71 TvContract.Programs.COLUMN_VIDEO_WIDTH, 72 TvContract.Programs.COLUMN_VIDEO_HEIGHT, 73 TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA 74 }; 75 76 // Columns which is deprecated in NYC 77 @SuppressWarnings("deprecation") 78 private static final String[] PROJECTION_DEPRECATED_IN_NYC = { 79 TvContract.Programs.COLUMN_SEASON_NUMBER, 80 TvContract.Programs.COLUMN_EPISODE_NUMBER 81 }; 82 83 private static final String[] PROJECTION_ADDED_IN_NYC = { 84 TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, 85 TvContract.Programs.COLUMN_SEASON_TITLE, 86 TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, 87 TvContract.Programs.COLUMN_RECORDING_PROHIBITED 88 }; 89 90 public static final String[] PROJECTION = createProjection(); 91 92 private static String[] createProjection() { 93 return CollectionUtils.concatAll( 94 PROJECTION_BASE, 95 Build.VERSION.SDK_INT >= Build.VERSION_CODES.N 96 ? PROJECTION_ADDED_IN_NYC 97 : PROJECTION_DEPRECATED_IN_NYC); 98 } 99 100 /** 101 * Returns the column index for {@code column}, -1 if the column doesn't exist. 102 */ 103 public static int getColumnIndex(String column) { 104 for (int i = 0; i < PROJECTION.length; ++i) { 105 if (PROJECTION[i].equals(column)) { 106 return i; 107 } 108 } 109 return -1; 110 } 111 112 /** 113 * Creates {@code Program} object from cursor. 114 * 115 * <p>The query that created the cursor MUST use {@link #PROJECTION}. 116 */ 117 public static Program fromCursor(Cursor cursor) { 118 // Columns read must match the order of match {@link #PROJECTION} 119 Builder builder = new Builder(); 120 int index = 0; 121 builder.setId(cursor.getLong(index++)); 122 String packageName = cursor.getString(index++); 123 builder.setPackageName(packageName); 124 builder.setChannelId(cursor.getLong(index++)); 125 builder.setTitle(cursor.getString(index++)); 126 builder.setEpisodeTitle(cursor.getString(index++)); 127 builder.setDescription(cursor.getString(index++)); 128 builder.setLongDescription(cursor.getString(index++)); 129 builder.setPosterArtUri(cursor.getString(index++)); 130 builder.setThumbnailUri(cursor.getString(index++)); 131 builder.setCanonicalGenres(cursor.getString(index++)); 132 builder.setContentRatings( 133 TvContentRatingCache.getInstance().getRatings(cursor.getString(index++))); 134 builder.setStartTimeUtcMillis(cursor.getLong(index++)); 135 builder.setEndTimeUtcMillis(cursor.getLong(index++)); 136 builder.setVideoWidth((int) cursor.getLong(index++)); 137 builder.setVideoHeight((int) cursor.getLong(index++)); 138 if (Utils.isInBundledPackageSet(packageName)) { 139 InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder); 140 } 141 index++; 142 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 143 builder.setSeasonNumber(cursor.getString(index++)); 144 builder.setSeasonTitle(cursor.getString(index++)); 145 builder.setEpisodeNumber(cursor.getString(index++)); 146 builder.setRecordingProhibited(cursor.getInt(index++) == 1); 147 } else { 148 builder.setSeasonNumber(cursor.getString(index++)); 149 builder.setEpisodeNumber(cursor.getString(index++)); 150 } 151 return builder.build(); 152 } 153 154 public static Program fromParcel(Parcel in) { 155 Program program = new Program(); 156 program.mId = in.readLong(); 157 program.mPackageName = in.readString(); 158 program.mChannelId = in.readLong(); 159 program.mTitle = in.readString(); 160 program.mSeriesId = in.readString(); 161 program.mEpisodeTitle = in.readString(); 162 program.mSeasonNumber = in.readString(); 163 program.mSeasonTitle = in.readString(); 164 program.mEpisodeNumber = in.readString(); 165 program.mStartTimeUtcMillis = in.readLong(); 166 program.mEndTimeUtcMillis = in.readLong(); 167 program.mDescription = in.readString(); 168 program.mLongDescription = in.readString(); 169 program.mVideoWidth = in.readInt(); 170 program.mVideoHeight = in.readInt(); 171 program.mCriticScores = in.readArrayList(Thread.currentThread().getContextClassLoader()); 172 program.mPosterArtUri = in.readString(); 173 program.mThumbnailUri = in.readString(); 174 program.mCanonicalGenreIds = in.createIntArray(); 175 int length = in.readInt(); 176 if (length > 0) { 177 program.mContentRatings = new TvContentRating[length]; 178 for (int i = 0; i < length; ++i) { 179 program.mContentRatings[i] = TvContentRating.unflattenFromString(in.readString()); 180 } 181 } 182 program.mRecordingProhibited = in.readByte() != (byte) 0; 183 return program; 184 } 185 186 public static final Parcelable.Creator<Program> CREATOR = new Parcelable.Creator<Program>() { 187 @Override 188 public Program createFromParcel(Parcel in) { 189 return Program.fromParcel(in); 190 } 191 192 @Override 193 public Program[] newArray(int size) { 194 return new Program[size]; 195 } 196 }; 197 198 private long mId; 199 private String mPackageName; 200 private long mChannelId; 201 private String mTitle; 202 private String mSeriesId; 203 private String mEpisodeTitle; 204 private String mSeasonNumber; 205 private String mSeasonTitle; 206 private String mEpisodeNumber; 207 private long mStartTimeUtcMillis; 208 private long mEndTimeUtcMillis; 209 private String mDescription; 210 private String mLongDescription; 211 private int mVideoWidth; 212 private int mVideoHeight; 213 private List<CriticScore> mCriticScores; 214 private String mPosterArtUri; 215 private String mThumbnailUri; 216 private int[] mCanonicalGenreIds; 217 private TvContentRating[] mContentRatings; 218 private boolean mRecordingProhibited; 219 220 private Program() { 221 // Do nothing. 222 } 223 224 public long getId() { 225 return mId; 226 } 227 228 /** 229 * Returns the package name of this program. 230 */ 231 public String getPackageName() { 232 return mPackageName; 233 } 234 235 public long getChannelId() { 236 return mChannelId; 237 } 238 239 /** 240 * Returns {@code true} if this program is valid or {@code false} otherwise. 241 */ 242 @Override 243 public boolean isValid() { 244 return mChannelId >= 0; 245 } 246 247 /** 248 * Returns {@code true} if the program is valid and {@code false} otherwise. 249 */ 250 public static boolean isValid(Program program) { 251 return program != null && program.isValid(); 252 } 253 254 @Override 255 public String getTitle() { 256 return mTitle; 257 } 258 259 /** 260 * Returns the series ID. 261 */ 262 @Override 263 public String getSeriesId() { 264 return mSeriesId; 265 } 266 267 /** 268 * Returns the episode title. 269 */ 270 @Override 271 public String getEpisodeTitle() { 272 return mEpisodeTitle; 273 } 274 275 @Override 276 public String getSeasonNumber() { 277 return mSeasonNumber; 278 } 279 280 @Override 281 public String getEpisodeNumber() { 282 return mEpisodeNumber; 283 } 284 285 @Override 286 public long getStartTimeUtcMillis() { 287 return mStartTimeUtcMillis; 288 } 289 290 @Override 291 public long getEndTimeUtcMillis() { 292 return mEndTimeUtcMillis; 293 } 294 295 /** 296 * Returns the program duration. 297 */ 298 @Override 299 public long getDurationMillis() { 300 return mEndTimeUtcMillis - mStartTimeUtcMillis; 301 } 302 303 @Override 304 public String getDescription() { 305 return mDescription; 306 } 307 308 @Override 309 public String getLongDescription() { 310 return mLongDescription; 311 } 312 313 public int getVideoWidth() { 314 return mVideoWidth; 315 } 316 317 public int getVideoHeight() { 318 return mVideoHeight; 319 } 320 321 /** 322 * Returns the list of Critic Scores for this program 323 */ 324 @Nullable 325 public List<CriticScore> getCriticScores() { 326 return mCriticScores; 327 } 328 329 @Nullable 330 @Override 331 public TvContentRating[] getContentRatings() { 332 return mContentRatings; 333 } 334 335 @Override 336 public String getPosterArtUri() { 337 return mPosterArtUri; 338 } 339 340 @Override 341 public String getThumbnailUri() { 342 return mThumbnailUri; 343 } 344 345 /** 346 * Returns {@code true} if the recording of this program is prohibited. 347 */ 348 public boolean isRecordingProhibited() { 349 return mRecordingProhibited; 350 } 351 352 /** 353 * Returns array of canonical genres for this program. 354 * This is expected to be called rarely. 355 */ 356 @Nullable 357 public String[] getCanonicalGenres() { 358 if (mCanonicalGenreIds == null) { 359 return null; 360 } 361 String[] genres = new String[mCanonicalGenreIds.length]; 362 for (int i = 0; i < mCanonicalGenreIds.length; i++) { 363 genres[i] = GenreItems.getCanonicalGenre(mCanonicalGenreIds[i]); 364 } 365 return genres; 366 } 367 368 /** 369 * Returns array of canonical genre ID's for this program. 370 */ 371 @Override 372 public int[] getCanonicalGenreIds() { 373 return mCanonicalGenreIds; 374 } 375 376 /** 377 * Returns if this program has the genre. 378 */ 379 public boolean hasGenre(int genreId) { 380 if (genreId == GenreItems.ID_ALL_CHANNELS) { 381 return true; 382 } 383 if (mCanonicalGenreIds != null) { 384 for (int id : mCanonicalGenreIds) { 385 if (id == genreId) { 386 return true; 387 } 388 } 389 } 390 return false; 391 } 392 393 @Override 394 public int hashCode() { 395 // Hash with all the properties because program ID can be invalid for the dummy programs. 396 return Objects.hash(mChannelId, mStartTimeUtcMillis, mEndTimeUtcMillis, 397 mTitle, mSeriesId, mEpisodeTitle, mDescription, mLongDescription, mVideoWidth, 398 mVideoHeight, mPosterArtUri, mThumbnailUri, Arrays.hashCode(mContentRatings), 399 Arrays.hashCode(mCanonicalGenreIds), mSeasonNumber, mSeasonTitle, mEpisodeNumber, 400 mRecordingProhibited); 401 } 402 403 @Override 404 public boolean equals(Object other) { 405 if (!(other instanceof Program)) { 406 return false; 407 } 408 // Compare all the properties because program ID can be invalid for the dummy programs. 409 Program program = (Program) other; 410 return Objects.equals(mPackageName, program.mPackageName) 411 && mChannelId == program.mChannelId 412 && mStartTimeUtcMillis == program.mStartTimeUtcMillis 413 && mEndTimeUtcMillis == program.mEndTimeUtcMillis 414 && Objects.equals(mTitle, program.mTitle) 415 && Objects.equals(mSeriesId, program.mSeriesId) 416 && Objects.equals(mEpisodeTitle, program.mEpisodeTitle) 417 && Objects.equals(mDescription, program.mDescription) 418 && Objects.equals(mLongDescription, program.mLongDescription) 419 && mVideoWidth == program.mVideoWidth 420 && mVideoHeight == program.mVideoHeight 421 && Objects.equals(mPosterArtUri, program.mPosterArtUri) 422 && Objects.equals(mThumbnailUri, program.mThumbnailUri) 423 && Arrays.equals(mContentRatings, program.mContentRatings) 424 && Arrays.equals(mCanonicalGenreIds, program.mCanonicalGenreIds) 425 && Objects.equals(mSeasonNumber, program.mSeasonNumber) 426 && Objects.equals(mSeasonTitle, program.mSeasonTitle) 427 && Objects.equals(mEpisodeNumber, program.mEpisodeNumber) 428 && mRecordingProhibited == program.mRecordingProhibited; 429 } 430 431 @Override 432 public int compareTo(@NonNull Program other) { 433 return Long.compare(mStartTimeUtcMillis, other.mStartTimeUtcMillis); 434 } 435 436 @Override 437 public String toString() { 438 StringBuilder builder = new StringBuilder(); 439 builder.append("Program[").append(mId) 440 .append("]{channelId=").append(mChannelId) 441 .append(", packageName=").append(mPackageName) 442 .append(", title=").append(mTitle) 443 .append(", seriesId=").append(mSeriesId) 444 .append(", episodeTitle=").append(mEpisodeTitle) 445 .append(", seasonNumber=").append(mSeasonNumber) 446 .append(", seasonTitle=").append(mSeasonTitle) 447 .append(", episodeNumber=").append(mEpisodeNumber) 448 .append(", startTimeUtcSec=").append(Utils.toTimeString(mStartTimeUtcMillis)) 449 .append(", endTimeUtcSec=").append(Utils.toTimeString(mEndTimeUtcMillis)) 450 .append(", videoWidth=").append(mVideoWidth) 451 .append(", videoHeight=").append(mVideoHeight) 452 .append(", contentRatings=") 453 .append(TvContentRatingCache.contentRatingsToString(mContentRatings)) 454 .append(", posterArtUri=").append(mPosterArtUri) 455 .append(", thumbnailUri=").append(mThumbnailUri) 456 .append(", canonicalGenres=").append(Arrays.toString(mCanonicalGenreIds)) 457 .append(", recordingProhibited=").append(mRecordingProhibited); 458 if (DEBUG_DUMP_DESCRIPTION) { 459 builder.append(", description=").append(mDescription) 460 .append(", longDescription=").append(mLongDescription); 461 } 462 return builder.append("}").toString(); 463 } 464 465 /** 466 * Translates a {@link Program} to {@link ContentValues} that are ready to be written into 467 * Database. 468 */ 469 @SuppressLint("InlinedApi") 470 @SuppressWarnings("deprecation") 471 public static ContentValues toContentValues(Program program) { 472 ContentValues values = new ContentValues(); 473 values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId()); 474 putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle()); 475 putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); 476 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 477 putValue(values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, 478 program.getSeasonNumber()); 479 putValue(values, TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, 480 program.getEpisodeNumber()); 481 } else { 482 putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); 483 putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); 484 } 485 putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); 486 putValue(values, TvContract.Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription()); 487 putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); 488 putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri()); 489 String[] canonicalGenres = program.getCanonicalGenres(); 490 if (canonicalGenres != null && canonicalGenres.length > 0) { 491 putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, 492 TvContract.Programs.Genres.encode(canonicalGenres)); 493 } else { 494 putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, ""); 495 } 496 putValue(values, Programs.COLUMN_CONTENT_RATING, 497 TvContentRatingCache.contentRatingsToString(program.getContentRatings())); 498 values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, 499 program.getStartTimeUtcMillis()); 500 values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); 501 putValue(values, TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA, 502 InternalDataUtils.serializeInternalProviderData(program)); 503 return values; 504 } 505 506 private static void putValue(ContentValues contentValues, String key, String value) { 507 if (TextUtils.isEmpty(value)) { 508 contentValues.putNull(key); 509 } else { 510 contentValues.put(key, value); 511 } 512 } 513 514 private static void putValue(ContentValues contentValues, String key, byte[] value) { 515 if (value == null || value.length == 0) { 516 contentValues.putNull(key); 517 } else { 518 contentValues.put(key, value); 519 } 520 } 521 522 public void copyFrom(Program other) { 523 if (this == other) { 524 return; 525 } 526 527 mId = other.mId; 528 mPackageName = other.mPackageName; 529 mChannelId = other.mChannelId; 530 mTitle = other.mTitle; 531 mSeriesId = other.mSeriesId; 532 mEpisodeTitle = other.mEpisodeTitle; 533 mSeasonNumber = other.mSeasonNumber; 534 mSeasonTitle = other.mSeasonTitle; 535 mEpisodeNumber = other.mEpisodeNumber; 536 mStartTimeUtcMillis = other.mStartTimeUtcMillis; 537 mEndTimeUtcMillis = other.mEndTimeUtcMillis; 538 mDescription = other.mDescription; 539 mLongDescription = other.mLongDescription; 540 mVideoWidth = other.mVideoWidth; 541 mVideoHeight = other.mVideoHeight; 542 mCriticScores = other.mCriticScores; 543 mPosterArtUri = other.mPosterArtUri; 544 mThumbnailUri = other.mThumbnailUri; 545 mCanonicalGenreIds = other.mCanonicalGenreIds; 546 mContentRatings = other.mContentRatings; 547 mRecordingProhibited = other.mRecordingProhibited; 548 } 549 550 /** 551 * A Builder for the Program class 552 */ 553 public static final class Builder { 554 private final Program mProgram; 555 556 /** 557 * Creates a Builder for this Program class 558 */ 559 public Builder() { 560 mProgram = new Program(); 561 // Fill initial data. 562 mProgram.mPackageName = null; 563 mProgram.mChannelId = Channel.INVALID_ID; 564 mProgram.mTitle = null; 565 mProgram.mSeasonNumber = null; 566 mProgram.mSeasonTitle = null; 567 mProgram.mEpisodeNumber = null; 568 mProgram.mStartTimeUtcMillis = -1; 569 mProgram.mEndTimeUtcMillis = -1; 570 mProgram.mDescription = null; 571 mProgram.mLongDescription = null; 572 mProgram.mRecordingProhibited = false; 573 mProgram.mCriticScores = null; 574 } 575 576 /** 577 * Creates a builder for this Program class 578 * by setting default values equivalent to another Program 579 * @param other the program to be copied 580 */ 581 @VisibleForTesting 582 public Builder(Program other) { 583 mProgram = new Program(); 584 mProgram.copyFrom(other); 585 } 586 587 /** 588 * Sets the ID of this program 589 * @param id the ID 590 * @return a reference to this object 591 */ 592 public Builder setId(long id) { 593 mProgram.mId = id; 594 return this; 595 } 596 597 /** 598 * Sets the package name for this program 599 * @param packageName the package name 600 * @return a reference to this object 601 */ 602 public Builder setPackageName(String packageName){ 603 mProgram.mPackageName = packageName; 604 return this; 605 } 606 607 /** 608 * Sets the channel ID for this program 609 * @param channelId the channel ID 610 * @return a reference to this object 611 */ 612 public Builder setChannelId(long channelId) { 613 mProgram.mChannelId = channelId; 614 return this; 615 } 616 617 /** 618 * Sets the program title 619 * @param title the title 620 * @return a reference to this object 621 */ 622 public Builder setTitle(String title) { 623 mProgram.mTitle = title; 624 return this; 625 } 626 627 /** 628 * Sets the series ID. 629 * @param seriesId the series ID 630 * @return a reference to this object 631 */ 632 public Builder setSeriesId(String seriesId) { 633 mProgram.mSeriesId = seriesId; 634 return this; 635 } 636 637 /** 638 * Sets the episode title if this is a series program 639 * @param episodeTitle the episode title 640 * @return a reference to this object 641 */ 642 public Builder setEpisodeTitle(String episodeTitle) { 643 mProgram.mEpisodeTitle = episodeTitle; 644 return this; 645 } 646 647 /** 648 * Sets the season number if this is a series program 649 * @param seasonNumber the season number 650 * @return a reference to this object 651 */ 652 public Builder setSeasonNumber(String seasonNumber) { 653 mProgram.mSeasonNumber = seasonNumber; 654 return this; 655 } 656 657 658 /** 659 * Sets the season title if this is a series program 660 * @param seasonTitle the season title 661 * @return a reference to this object 662 */ 663 public Builder setSeasonTitle(String seasonTitle) { 664 mProgram.mSeasonTitle = seasonTitle; 665 return this; 666 } 667 668 /** 669 * Sets the episode number if this is a series program 670 * @param episodeNumber the episode number 671 * @return a reference to this object 672 */ 673 public Builder setEpisodeNumber(String episodeNumber) { 674 mProgram.mEpisodeNumber = episodeNumber; 675 return this; 676 } 677 678 /** 679 * Sets the start time of this program 680 * @param startTimeUtcMillis the start time in UTC milliseconds 681 * @return a reference to this object 682 */ 683 public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { 684 mProgram.mStartTimeUtcMillis = startTimeUtcMillis; 685 return this; 686 } 687 688 /** 689 * Sets the end time of this program 690 * @param endTimeUtcMillis the end time in UTC milliseconds 691 * @return a reference to this object 692 */ 693 public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { 694 mProgram.mEndTimeUtcMillis = endTimeUtcMillis; 695 return this; 696 } 697 698 /** 699 * Sets a description 700 * @param description the description 701 * @return a reference to this object 702 */ 703 public Builder setDescription(String description) { 704 mProgram.mDescription = description; 705 return this; 706 } 707 708 /** 709 * Sets a long description 710 * @param longDescription the long description 711 * @return a reference to this object 712 */ 713 public Builder setLongDescription(String longDescription) { 714 mProgram.mLongDescription = longDescription; 715 return this; 716 } 717 718 /** 719 * Defines the video width of this program 720 * @param width 721 * @return a reference to this object 722 */ 723 public Builder setVideoWidth(int width) { 724 mProgram.mVideoWidth = width; 725 return this; 726 } 727 728 /** 729 * Defines the video height of this program 730 * @param height 731 * @return a reference to this object 732 */ 733 public Builder setVideoHeight(int height) { 734 mProgram.mVideoHeight = height; 735 return this; 736 } 737 738 /** 739 * Sets the content ratings for this program 740 * @param contentRatings the content ratings 741 * @return a reference to this object 742 */ 743 public Builder setContentRatings(TvContentRating[] contentRatings) { 744 mProgram.mContentRatings = contentRatings; 745 return this; 746 } 747 748 /** 749 * Sets the poster art URI 750 * @param posterArtUri the poster art URI 751 * @return a reference to this object 752 */ 753 public Builder setPosterArtUri(String posterArtUri) { 754 mProgram.mPosterArtUri = posterArtUri; 755 return this; 756 } 757 758 /** 759 * Sets the thumbnail URI 760 * @param thumbnailUri the thumbnail URI 761 * @return a reference to this object 762 */ 763 public Builder setThumbnailUri(String thumbnailUri) { 764 mProgram.mThumbnailUri = thumbnailUri; 765 return this; 766 } 767 768 /** 769 * Sets the canonical genres by id 770 * @param genres the genres 771 * @return a reference to this object 772 */ 773 public Builder setCanonicalGenres(String genres) { 774 mProgram.mCanonicalGenreIds = Utils.getCanonicalGenreIds(genres); 775 return this; 776 } 777 778 /** 779 * Sets the recording prohibited flag 780 * @param recordingProhibited recording prohibited flag 781 * @return a reference to this object 782 */ 783 public Builder setRecordingProhibited(boolean recordingProhibited) { 784 mProgram.mRecordingProhibited = recordingProhibited; 785 return this; 786 } 787 788 /** 789 * Adds a critic score 790 * @param criticScore the critic score 791 * @return a reference to this object 792 */ 793 public Builder addCriticScore(CriticScore criticScore) { 794 if (criticScore.score != null) { 795 if (mProgram.mCriticScores == null) { 796 mProgram.mCriticScores = new ArrayList<>(); 797 } 798 mProgram.mCriticScores.add(criticScore); 799 } 800 return this; 801 } 802 803 /** 804 * Sets the critic scores 805 * @param criticScores the critic scores 806 * @return a reference to this objects 807 */ 808 public Builder setCriticScores(List<CriticScore> criticScores) { 809 mProgram.mCriticScores = criticScores; 810 return this; 811 } 812 813 /** 814 * Returns a reference to the Program object being constructed 815 * @return the Program object constructed 816 */ 817 public Program build() { 818 // Generate the series ID for the episodic program of other TV input. 819 if (TextUtils.isEmpty(mProgram.mTitle)) { 820 // If title is null, series cannot be generated for this program. 821 setSeriesId(null); 822 } else if (TextUtils.isEmpty(mProgram.mSeriesId) 823 && !TextUtils.isEmpty(mProgram.mEpisodeNumber)) { 824 // If series ID is not set, generate it for the episodic program of other TV input. 825 setSeriesId(BaseProgram.generateSeriesId(mProgram.mPackageName, mProgram.mTitle)); 826 } 827 Program program = new Program(); 828 program.copyFrom(mProgram); 829 return program; 830 } 831 } 832 833 /** 834 * Prefetches the program poster art.<p> 835 */ 836 public void prefetchPosterArt(Context context, int posterArtWidth, int posterArtHeight) { 837 if (mPosterArtUri == null) { 838 return; 839 } 840 ImageLoader.prefetchBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight); 841 } 842 843 /** 844 * Loads the program poster art and returns it via {@code callback}. 845 * <p> 846 * Note that it may directly call {@code callback} if the program poster art already is loaded. 847 * 848 * @return {@code true} if the load is complete and the callback is executed. 849 */ 850 @UiThread 851 public boolean loadPosterArt(Context context, int posterArtWidth, int posterArtHeight, 852 ImageLoader.ImageLoaderCallback callback) { 853 if (mPosterArtUri == null) { 854 return false; 855 } 856 return ImageLoader.loadBitmap( 857 context, mPosterArtUri, posterArtWidth, posterArtHeight, callback); 858 } 859 860 public static boolean isDuplicate(Program p1, Program p2) { 861 if (p1 == null || p2 == null) { 862 return false; 863 } 864 boolean isDuplicate = p1.getChannelId() == p2.getChannelId() 865 && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis() 866 && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis(); 867 if (DEBUG && BuildConfig.ENG && isDuplicate) { 868 Log.w(TAG, "Duplicate programs detected! - \"" + p1.getTitle() + "\" and \"" 869 + p2.getTitle() + "\""); 870 } 871 return isDuplicate; 872 } 873 874 @Override 875 public int describeContents() { 876 return 0; 877 } 878 879 @Override 880 public void writeToParcel(Parcel out, int paramInt) { 881 out.writeLong(mId); 882 out.writeString(mPackageName); 883 out.writeLong(mChannelId); 884 out.writeString(mTitle); 885 out.writeString(mSeriesId); 886 out.writeString(mEpisodeTitle); 887 out.writeString(mSeasonNumber); 888 out.writeString(mSeasonTitle); 889 out.writeString(mEpisodeNumber); 890 out.writeLong(mStartTimeUtcMillis); 891 out.writeLong(mEndTimeUtcMillis); 892 out.writeString(mDescription); 893 out.writeString(mLongDescription); 894 out.writeInt(mVideoWidth); 895 out.writeInt(mVideoHeight); 896 out.writeList(mCriticScores); 897 out.writeString(mPosterArtUri); 898 out.writeString(mThumbnailUri); 899 out.writeIntArray(mCanonicalGenreIds); 900 out.writeInt(mContentRatings == null ? 0 : mContentRatings.length); 901 if (mContentRatings != null) { 902 for (TvContentRating rating : mContentRatings) { 903 out.writeString(rating.flattenToString()); 904 } 905 } 906 out.writeByte((byte) (mRecordingProhibited ? 1 : 0)); 907 } 908 909 /** 910 * Holds one type of critic score and its source. 911 */ 912 public static final class CriticScore implements Serializable, Parcelable { 913 /** 914 * The source of the rating. 915 */ 916 public final String source; 917 /** 918 * The score. 919 */ 920 public final String score; 921 /** 922 * The url of the logo image 923 */ 924 public final String logoUrl; 925 926 public static final Parcelable.Creator<CriticScore> CREATOR = 927 new Parcelable.Creator<CriticScore>() { 928 @Override 929 public CriticScore createFromParcel(Parcel in) { 930 String source = in.readString(); 931 String score = in.readString(); 932 String logoUri = in.readString(); 933 return new CriticScore(source, score, logoUri); 934 } 935 936 @Override 937 public CriticScore[] newArray(int size) { 938 return new CriticScore[size]; 939 } 940 }; 941 942 /** 943 * Constructor for this class. 944 * @param source the source of the rating 945 * @param score the score 946 */ 947 public CriticScore(String source, String score, String logoUrl) { 948 this.source = source; 949 this.score = score; 950 this.logoUrl = logoUrl; 951 } 952 953 @Override 954 public int describeContents() { 955 return 0; 956 } 957 958 @Override 959 public void writeToParcel(Parcel out, int i) { 960 out.writeString(source); 961 out.writeString(score); 962 out.writeString(logoUrl); 963 } 964 } 965 } 966