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