/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tv.data; import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.media.tv.TvContentRating; import android.media.tv.TvContract; import android.media.tv.TvContract.Programs; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.text.TextUtils; import com.android.tv.common.TvContentRatingCache; import com.android.tv.common.util.CollectionUtils; import com.android.tv.common.util.CommonUtils; import com.android.tv.data.api.BaseProgram; import com.android.tv.data.api.Channel; import com.android.tv.data.api.Program; import com.android.tv.util.TvProviderUtils; import com.android.tv.util.Utils; import com.android.tv.util.images.ImageLoader; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; /** A convenience class to create and insert program information entries into the database. */ public final class ProgramImpl extends BaseProgramImpl implements Parcelable, Program { private static final boolean DEBUG = false; private static final boolean DEBUG_DUMP_DESCRIPTION = false; private static final String TAG = "Program"; private static final String[] PROJECTION_BASE = { // Columns must match what is read in Program.fromCursor() TvContract.Programs._ID, TvContract.Programs.COLUMN_PACKAGE_NAME, TvContract.Programs.COLUMN_CHANNEL_ID, TvContract.Programs.COLUMN_TITLE, TvContract.Programs.COLUMN_EPISODE_TITLE, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, TvContract.Programs.COLUMN_LONG_DESCRIPTION, TvContract.Programs.COLUMN_POSTER_ART_URI, TvContract.Programs.COLUMN_THUMBNAIL_URI, TvContract.Programs.COLUMN_CANONICAL_GENRE, TvContract.Programs.COLUMN_CONTENT_RATING, TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, TvContract.Programs.COLUMN_VIDEO_WIDTH, TvContract.Programs.COLUMN_VIDEO_HEIGHT, TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA }; // Columns which is deprecated in NYC @SuppressWarnings("deprecation") private static final String[] PROJECTION_DEPRECATED_IN_NYC = { TvContract.Programs.COLUMN_SEASON_NUMBER, TvContract.Programs.COLUMN_EPISODE_NUMBER }; private static final String[] PROJECTION_ADDED_IN_NYC = { TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, TvContract.Programs.COLUMN_SEASON_TITLE, TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, TvContract.Programs.COLUMN_RECORDING_PROHIBITED }; public static final String[] PROJECTION = createProjection(); public static final String[] PARTIAL_PROJECTION = { TvContract.Programs._ID, TvContract.Programs.COLUMN_CHANNEL_ID, TvContract.Programs.COLUMN_TITLE, TvContract.Programs.COLUMN_EPISODE_TITLE, TvContract.Programs.COLUMN_CANONICAL_GENRE, TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, }; private static String[] createProjection() { return CollectionUtils.concatAll( PROJECTION_BASE, Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? PROJECTION_ADDED_IN_NYC : PROJECTION_DEPRECATED_IN_NYC); } /** * Returns the column index for {@code column},-1 if the column doesn't exist in {@link * #PROJECTION}. */ public static int getColumnIndex(String column) { for (int i = 0; i < PROJECTION.length; ++i) { if (PROJECTION[i].equals(column)) { return i; } } return -1; } /** Creates {@code Program} object from cursor. */ public static Program fromCursor(Cursor cursor) { // Columns read must match the order of match {@link #PROJECTION} Builder builder = new Builder(); int index = 0; builder.setId(cursor.getLong(index++)); String packageName = cursor.getString(index++); builder.setPackageName(packageName); builder.setChannelId(cursor.getLong(index++)); builder.setTitle(cursor.getString(index++)); builder.setEpisodeTitle(cursor.getString(index++)); builder.setDescription(cursor.getString(index++)); builder.setLongDescription(cursor.getString(index++)); builder.setPosterArtUri(cursor.getString(index++)); builder.setThumbnailUri(cursor.getString(index++)); builder.setCanonicalGenres(cursor.getString(index++)); builder.setContentRatings( TvContentRatingCache.getInstance().getRatings(cursor.getString(index++))); builder.setStartTimeUtcMillis(cursor.getLong(index++)); builder.setEndTimeUtcMillis(cursor.getLong(index++)); builder.setVideoWidth((int) cursor.getLong(index++)); builder.setVideoHeight((int) cursor.getLong(index++)); if (CommonUtils.isInBundledPackageSet(packageName)) { InternalDataUtils.deserializeInternalProviderData(cursor.getBlob(index), builder); } index++; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { builder.setSeasonNumber(cursor.getString(index++)); builder.setSeasonTitle(cursor.getString(index++)); builder.setEpisodeNumber(cursor.getString(index++)); builder.setRecordingProhibited(cursor.getInt(index++) == 1); } else { builder.setSeasonNumber(cursor.getString(index++)); builder.setEpisodeNumber(cursor.getString(index++)); } if (TvProviderUtils.getProgramHasSeriesIdColumn()) { String seriesId = cursor.getString(index); if (!TextUtils.isEmpty(seriesId)) { builder.setSeriesId(seriesId); } } return builder.build(); } /** Creates {@code Program} object from cursor. */ public static Program fromCursorPartialProjection(Cursor cursor) { // Columns read must match the order of match {@link #PARTIAL_PROJECTION} Builder builder = new Builder(); int index = 0; builder.setId(cursor.getLong(index++)); builder.setChannelId(cursor.getLong(index++)); builder.setTitle(cursor.getString(index++)); builder.setEpisodeTitle(cursor.getString(index++)); builder.setCanonicalGenres(cursor.getString(index++)); builder.setStartTimeUtcMillis(cursor.getLong(index++)); builder.setEndTimeUtcMillis(cursor.getLong(index++)); return builder.build(); } public static ProgramImpl fromParcel(Parcel in) { ProgramImpl program = new ProgramImpl(); program.mId = in.readLong(); program.mPackageName = in.readString(); program.mChannelId = in.readLong(); program.mTitle = in.readString(); program.mSeriesId = in.readString(); program.mEpisodeTitle = in.readString(); program.mSeasonNumber = in.readString(); program.mSeasonTitle = in.readString(); program.mEpisodeNumber = in.readString(); program.mStartTimeUtcMillis = in.readLong(); program.mEndTimeUtcMillis = in.readLong(); program.mDescription = in.readString(); program.mLongDescription = in.readString(); program.mVideoWidth = in.readInt(); program.mVideoHeight = in.readInt(); program.mCriticScores = in.readArrayList(Thread.currentThread().getContextClassLoader()); program.mPosterArtUri = in.readString(); program.mThumbnailUri = in.readString(); program.mCanonicalGenreIds = in.createIntArray(); int length = in.readInt(); if (length > 0) { ImmutableList.Builder ratingsBuilder = ImmutableList.builderWithExpectedSize(length); for (int i = 0; i < length; ++i) { ratingsBuilder.add(TvContentRating.unflattenFromString(in.readString())); } program.mContentRatings = ratingsBuilder.build(); } else { program.mContentRatings = ImmutableList.of(); } program.mRecordingProhibited = in.readByte() != (byte) 0; return program; } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public Program createFromParcel(Parcel in) { return ProgramImpl.fromParcel(in); } @Override public Program[] newArray(int size) { return new Program[size]; } }; private long mId; private String mPackageName; private long mChannelId; private String mTitle; private String mSeriesId; private String mEpisodeTitle; private String mSeasonNumber; private String mSeasonTitle; private String mEpisodeNumber; private long mStartTimeUtcMillis; private long mEndTimeUtcMillis; private String mDurationString; private String mDescription; private String mLongDescription; private int mVideoWidth; private int mVideoHeight; private List mCriticScores; private String mPosterArtUri; private String mThumbnailUri; private int[] mCanonicalGenreIds; private ImmutableList mContentRatings; private boolean mRecordingProhibited; private ProgramImpl() { // Do nothing. } @Override public long getId() { return mId; } @Override public String getPackageName() { return mPackageName; } @Override public long getChannelId() { return mChannelId; } /** Returns {@code true} if this program is valid or {@code false} otherwise. */ @Override public boolean isValid() { return mChannelId >= 0; } @Override public String getTitle() { return mTitle; } /** Returns the series ID. */ @Override public String getSeriesId() { return mSeriesId; } /** Returns the episode title. */ @Override public String getEpisodeTitle() { return mEpisodeTitle; } @Override public String getSeasonNumber() { return mSeasonNumber; } @Override public String getSeasonTitle() { return mSeasonTitle; } @Override public String getEpisodeNumber() { return mEpisodeNumber; } @Override public long getStartTimeUtcMillis() { return mStartTimeUtcMillis; } @Override public long getEndTimeUtcMillis() { return mEndTimeUtcMillis; } @Override public String getDurationString(Context context) { // TODO(b/71717446): expire the calculated string if (mDurationString == null) { mDurationString = Utils.getDurationString(context, mStartTimeUtcMillis, mEndTimeUtcMillis, true); } return mDurationString; } /** Returns the program duration. */ @Override public long getDurationMillis() { return mEndTimeUtcMillis - mStartTimeUtcMillis; } @Override public String getDescription() { return mDescription; } @Override public String getLongDescription() { return mLongDescription; } @Override public int getVideoWidth() { return mVideoWidth; } @Override public int getVideoHeight() { return mVideoHeight; } @Override @Nullable public List getCriticScores() { return mCriticScores; } @Nullable @Override public ImmutableList getContentRatings() { return mContentRatings; } @Override public String getPosterArtUri() { return mPosterArtUri; } @Override public String getThumbnailUri() { return mThumbnailUri; } @Override public boolean isRecordingProhibited() { return mRecordingProhibited; } @Override @Nullable public String[] getCanonicalGenres() { if (mCanonicalGenreIds == null) { return null; } String[] genres = new String[mCanonicalGenreIds.length]; for (int i = 0; i < mCanonicalGenreIds.length; i++) { genres[i] = GenreItems.getCanonicalGenre(mCanonicalGenreIds[i]); } return genres; } /** Returns array of canonical genre ID's for this program. */ @Override public int[] getCanonicalGenreIds() { return mCanonicalGenreIds; } @Override public boolean hasGenre(int genreId) { if (genreId == GenreItems.ID_ALL_CHANNELS) { return true; } if (mCanonicalGenreIds != null) { for (int id : mCanonicalGenreIds) { if (id == genreId) { return true; } } } return false; } @Override public int hashCode() { // Hash with all the properties because program ID can be invalid for the dummy programs. return Objects.hash( mChannelId, mStartTimeUtcMillis, mEndTimeUtcMillis, mTitle, mSeriesId, mEpisodeTitle, mDescription, mLongDescription, mVideoWidth, mVideoHeight, mPosterArtUri, mThumbnailUri, mContentRatings, Arrays.hashCode(mCanonicalGenreIds), mSeasonNumber, mSeasonTitle, mEpisodeNumber, mRecordingProhibited); } @Override public boolean equals(Object other) { if (!(other instanceof ProgramImpl)) { return false; } // Compare all the properties because program ID can be invalid for the dummy programs. ProgramImpl program = (ProgramImpl) other; return Objects.equals(mPackageName, program.mPackageName) && mChannelId == program.mChannelId && mStartTimeUtcMillis == program.mStartTimeUtcMillis && mEndTimeUtcMillis == program.mEndTimeUtcMillis && Objects.equals(mTitle, program.mTitle) && Objects.equals(mSeriesId, program.mSeriesId) && Objects.equals(mEpisodeTitle, program.mEpisodeTitle) && Objects.equals(mDescription, program.mDescription) && Objects.equals(mLongDescription, program.mLongDescription) && mVideoWidth == program.mVideoWidth && mVideoHeight == program.mVideoHeight && Objects.equals(mPosterArtUri, program.mPosterArtUri) && Objects.equals(mThumbnailUri, program.mThumbnailUri) && Objects.equals(mContentRatings, program.mContentRatings) && Arrays.equals(mCanonicalGenreIds, program.mCanonicalGenreIds) && Objects.equals(mSeasonNumber, program.mSeasonNumber) && Objects.equals(mSeasonTitle, program.mSeasonTitle) && Objects.equals(mEpisodeNumber, program.mEpisodeNumber) && mRecordingProhibited == program.mRecordingProhibited; } @Override public int compareTo(@NonNull Program other) { return Long.compare(mStartTimeUtcMillis, other.getStartTimeUtcMillis()); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("Program[") .append(mId) .append("]{channelId=") .append(mChannelId) .append(", packageName=") .append(mPackageName) .append(", title=") .append(mTitle) .append(", seriesId=") .append(mSeriesId) .append(", episodeTitle=") .append(mEpisodeTitle) .append(", seasonNumber=") .append(mSeasonNumber) .append(", seasonTitle=") .append(mSeasonTitle) .append(", episodeNumber=") .append(mEpisodeNumber) .append(", startTimeUtcSec=") .append(Utils.toTimeString(mStartTimeUtcMillis)) .append(", endTimeUtcSec=") .append(Utils.toTimeString(mEndTimeUtcMillis)) .append(", videoWidth=") .append(mVideoWidth) .append(", videoHeight=") .append(mVideoHeight) .append(", contentRatings=") .append(TvContentRatingCache.contentRatingsToString(mContentRatings)) .append(", posterArtUri=") .append(mPosterArtUri) .append(", thumbnailUri=") .append(mThumbnailUri) .append(", canonicalGenres=") .append(Arrays.toString(mCanonicalGenreIds)) .append(", recordingProhibited=") .append(mRecordingProhibited); if (DEBUG_DUMP_DESCRIPTION) { builder.append(", description=") .append(mDescription) .append(", longDescription=") .append(mLongDescription); } return builder.append("}").toString(); } /** * Translates a {@link ProgramImpl} to {@link ContentValues} that are ready to be written into * Database. */ @SuppressLint("InlinedApi") @SuppressWarnings("deprecation") @WorkerThread public static ContentValues toContentValues(Program program, Context context) { ContentValues values = new ContentValues(); values.put(TvContract.Programs.COLUMN_CHANNEL_ID, program.getChannelId()); if (!TextUtils.isEmpty(program.getPackageName())) { values.put(Programs.COLUMN_PACKAGE_NAME, program.getPackageName()); } putValue(values, TvContract.Programs.COLUMN_TITLE, program.getTitle()); putValue(values, TvContract.Programs.COLUMN_EPISODE_TITLE, program.getEpisodeTitle()); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { putValue( values, TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, program.getSeasonNumber()); putValue( values, TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, program.getEpisodeNumber()); } else { putValue(values, TvContract.Programs.COLUMN_SEASON_NUMBER, program.getSeasonNumber()); putValue(values, TvContract.Programs.COLUMN_EPISODE_NUMBER, program.getEpisodeNumber()); } if (TvProviderUtils.checkSeriesIdColumn(context, Programs.CONTENT_URI)) { putValue(values, COLUMN_SERIES_ID, program.getSeriesId()); } putValue(values, TvContract.Programs.COLUMN_SHORT_DESCRIPTION, program.getDescription()); putValue(values, TvContract.Programs.COLUMN_LONG_DESCRIPTION, program.getLongDescription()); putValue(values, TvContract.Programs.COLUMN_POSTER_ART_URI, program.getPosterArtUri()); putValue(values, TvContract.Programs.COLUMN_THUMBNAIL_URI, program.getThumbnailUri()); String[] canonicalGenres = program.getCanonicalGenres(); if (canonicalGenres != null && canonicalGenres.length > 0) { putValue( values, TvContract.Programs.COLUMN_CANONICAL_GENRE, TvContract.Programs.Genres.encode(canonicalGenres)); } else { putValue(values, TvContract.Programs.COLUMN_CANONICAL_GENRE, ""); } putValue( values, Programs.COLUMN_CONTENT_RATING, TvContentRatingCache.contentRatingsToString(program.getContentRatings())); values.put( TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, program.getStartTimeUtcMillis()); values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, program.getEndTimeUtcMillis()); putValue( values, TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA, InternalDataUtils.serializeInternalProviderData(program)); return values; } private static void putValue(ContentValues contentValues, String key, String value) { if (TextUtils.isEmpty(value)) { contentValues.putNull(key); } else { contentValues.put(key, value); } } private static void putValue(ContentValues contentValues, String key, byte[] value) { if (value == null || value.length == 0) { contentValues.putNull(key); } else { contentValues.put(key, value); } } public void copyFrom(Program other) { if (this == other) { return; } mId = other.getId(); mPackageName = other.getPackageName(); mChannelId = other.getChannelId(); mTitle = other.getTitle(); mSeriesId = other.getSeriesId(); mEpisodeTitle = other.getEpisodeTitle(); mSeasonNumber = other.getSeasonNumber(); mSeasonTitle = other.getSeasonTitle(); mEpisodeNumber = other.getEpisodeNumber(); mStartTimeUtcMillis = other.getStartTimeUtcMillis(); mEndTimeUtcMillis = other.getEndTimeUtcMillis(); mDurationString = null; // Recreate Duration when needed. mDescription = other.getDescription(); mLongDescription = other.getLongDescription(); mVideoWidth = other.getVideoWidth(); mVideoHeight = other.getVideoHeight(); mCriticScores = other.getCriticScores(); mPosterArtUri = other.getPosterArtUri(); mThumbnailUri = other.getThumbnailUri(); mCanonicalGenreIds = other.getCanonicalGenreIds(); mContentRatings = other.getContentRatings(); mRecordingProhibited = other.isRecordingProhibited(); } /** A Builder for the Program class */ public static final class Builder { private final ProgramImpl mProgram; /** Creates a Builder for this Program class */ public Builder() { mProgram = new ProgramImpl(); // Fill initial data. mProgram.mPackageName = null; mProgram.mChannelId = Channel.INVALID_ID; mProgram.mTitle = null; mProgram.mSeasonNumber = null; mProgram.mSeasonTitle = null; mProgram.mEpisodeNumber = null; mProgram.mStartTimeUtcMillis = -1; mProgram.mEndTimeUtcMillis = -1; mProgram.mDurationString = null; mProgram.mDescription = null; mProgram.mLongDescription = null; mProgram.mRecordingProhibited = false; mProgram.mCriticScores = null; } /** * Creates a builder for this Program class by setting default values equivalent to another * Program * * @param other the program to be copied */ @VisibleForTesting public Builder(Program other) { mProgram = new ProgramImpl(); mProgram.copyFrom(other); } /** * Sets the ID of this program * * @param id the ID * @return a reference to this object */ public Builder setId(long id) { mProgram.mId = id; return this; } /** * Sets the package name for this program * * @param packageName the package name * @return a reference to this object */ public Builder setPackageName(String packageName) { mProgram.mPackageName = packageName; return this; } /** * Sets the channel ID for this program * * @param channelId the channel ID * @return a reference to this object */ public Builder setChannelId(long channelId) { mProgram.mChannelId = channelId; return this; } /** * Sets the program title * * @param title the title * @return a reference to this object */ public Builder setTitle(String title) { mProgram.mTitle = title; return this; } /** * Sets the series ID. * * @param seriesId the series ID * @return a reference to this object */ public Builder setSeriesId(String seriesId) { mProgram.mSeriesId = seriesId; return this; } /** * Sets the episode title if this is a series program * * @param episodeTitle the episode title * @return a reference to this object */ public Builder setEpisodeTitle(String episodeTitle) { mProgram.mEpisodeTitle = episodeTitle; return this; } /** * Sets the season number if this is a series program * * @param seasonNumber the season number * @return a reference to this object */ public Builder setSeasonNumber(String seasonNumber) { mProgram.mSeasonNumber = seasonNumber; return this; } /** * Sets the season title if this is a series program * * @param seasonTitle the season title * @return a reference to this object */ public Builder setSeasonTitle(String seasonTitle) { mProgram.mSeasonTitle = seasonTitle; return this; } /** * Sets the episode number if this is a series program * * @param episodeNumber the episode number * @return a reference to this object */ public Builder setEpisodeNumber(String episodeNumber) { mProgram.mEpisodeNumber = episodeNumber; return this; } /** * Sets the start time of this program * * @param startTimeUtcMillis the start time in UTC milliseconds * @return a reference to this object */ public Builder setStartTimeUtcMillis(long startTimeUtcMillis) { mProgram.mStartTimeUtcMillis = startTimeUtcMillis; return this; } /** * Sets the end time of this program * * @param endTimeUtcMillis the end time in UTC milliseconds * @return a reference to this object */ public Builder setEndTimeUtcMillis(long endTimeUtcMillis) { mProgram.mEndTimeUtcMillis = endTimeUtcMillis; return this; } /** * Sets a description * * @param description the description * @return a reference to this object */ public Builder setDescription(String description) { mProgram.mDescription = description; return this; } /** * Sets a long description * * @param longDescription the long description * @return a reference to this object */ public Builder setLongDescription(String longDescription) { mProgram.mLongDescription = longDescription; return this; } /** * Defines the video width of this program * * @param width * @return a reference to this object */ public Builder setVideoWidth(int width) { mProgram.mVideoWidth = width; return this; } /** * Defines the video height of this program * * @param height * @return a reference to this object */ public Builder setVideoHeight(int height) { mProgram.mVideoHeight = height; return this; } /** * Sets the content ratings for this program * * @param contentRatings the content ratings * @return a reference to this object */ public Builder setContentRatings(ImmutableList contentRatings) { mProgram.mContentRatings = contentRatings; return this; } /** * Sets the poster art URI * * @param posterArtUri the poster art URI * @return a reference to this object */ public Builder setPosterArtUri(String posterArtUri) { mProgram.mPosterArtUri = posterArtUri; return this; } /** * Sets the thumbnail URI * * @param thumbnailUri the thumbnail URI * @return a reference to this object */ public Builder setThumbnailUri(String thumbnailUri) { mProgram.mThumbnailUri = thumbnailUri; return this; } /** * Sets the canonical genres by id * * @param genres the genres * @return a reference to this object */ public Builder setCanonicalGenres(String genres) { mProgram.mCanonicalGenreIds = Utils.getCanonicalGenreIds(genres); return this; } /** * Sets the recording prohibited flag * * @param recordingProhibited recording prohibited flag * @return a reference to this object */ public Builder setRecordingProhibited(boolean recordingProhibited) { mProgram.mRecordingProhibited = recordingProhibited; return this; } /** * Adds a critic score * * @param criticScore the critic score * @return a reference to this object */ public Builder addCriticScore(CriticScore criticScore) { if (criticScore.score != null) { if (mProgram.mCriticScores == null) { mProgram.mCriticScores = new ArrayList<>(); } mProgram.mCriticScores.add(criticScore); } return this; } /** * Sets the critic scores * * @param criticScores the critic scores * @return a reference to this objects */ public Builder setCriticScores(List criticScores) { mProgram.mCriticScores = criticScores; return this; } /** * Returns a reference to the Program object being constructed * * @return the Program object constructed */ public ProgramImpl build() { // Generate the series ID for the episodic program of other TV input. if (TextUtils.isEmpty(mProgram.mTitle)) { // If title is null, series cannot be generated for this program. setSeriesId(null); } else if (TextUtils.isEmpty(mProgram.mSeriesId) && !TextUtils.isEmpty(mProgram.mEpisodeNumber)) { // If series ID is not set, generate it for the episodic program of other TV input. setSeriesId(BaseProgram.generateSeriesId(mProgram.mPackageName, mProgram.mTitle)); } ProgramImpl program = new ProgramImpl(); program.copyFrom(mProgram); return program; } } @Override public void prefetchPosterArt(Context context, int posterArtWidth, int posterArtHeight) { if (mPosterArtUri == null) { return; } ImageLoader.prefetchBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight); } @Override @UiThread public boolean loadPosterArt( Context context, int posterArtWidth, int posterArtHeight, ImageLoader.ImageLoaderCallback callback) { if (mPosterArtUri == null) { return false; } return ImageLoader.loadBitmap( context, mPosterArtUri, posterArtWidth, posterArtHeight, callback); } @Override public Parcelable toParcelable() { return this; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel out, int paramInt) { out.writeLong(mId); out.writeString(mPackageName); out.writeLong(mChannelId); out.writeString(mTitle); out.writeString(mSeriesId); out.writeString(mEpisodeTitle); out.writeString(mSeasonNumber); out.writeString(mSeasonTitle); out.writeString(mEpisodeNumber); out.writeLong(mStartTimeUtcMillis); out.writeLong(mEndTimeUtcMillis); out.writeString(mDescription); out.writeString(mLongDescription); out.writeInt(mVideoWidth); out.writeInt(mVideoHeight); out.writeList(mCriticScores); out.writeString(mPosterArtUri); out.writeString(mThumbnailUri); out.writeIntArray(mCanonicalGenreIds); out.writeInt(mContentRatings == null ? 0 : mContentRatings.size()); if (mContentRatings != null) { for (TvContentRating rating : mContentRatings) { out.writeString(rating.flattenToString()); } } out.writeByte((byte) (mRecordingProhibited ? 1 : 0)); } }