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