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