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