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