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