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
17package com.android.tv.data;
18
19import android.annotation.SuppressLint;
20import android.content.ContentValues;
21import android.content.Context;
22import android.database.Cursor;
23import android.media.tv.TvContentRating;
24import android.media.tv.TvContract;
25import android.media.tv.TvContract.Programs;
26import android.os.Build;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.support.annotation.NonNull;
30import android.support.annotation.Nullable;
31import android.support.annotation.UiThread;
32import android.support.annotation.VisibleForTesting;
33import android.text.TextUtils;
34import android.util.Log;
35
36import com.android.tv.common.BuildConfig;
37import com.android.tv.common.CollectionUtils;
38import com.android.tv.common.TvContentRatingCache;
39import com.android.tv.util.ImageLoader;
40import com.android.tv.util.Utils;
41
42import java.io.Serializable;
43import java.util.ArrayList;
44import java.util.Arrays;
45import java.util.List;
46import java.util.Objects;
47
48/**
49 * A convenience class to create and insert program information entries into the database.
50 */
51public 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