1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.data;
18 
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.media.tv.TvContentRating;
22 import android.media.tv.TvContract;
23 import android.support.annotation.NonNull;
24 import android.support.annotation.UiThread;
25 import android.support.v4.os.BuildCompat;
26 import android.text.TextUtils;
27 import android.util.Log;
28 
29 import com.android.tv.R;
30 import com.android.tv.common.BuildConfig;
31 import com.android.tv.common.CollectionUtils;
32 import com.android.tv.common.TvContentRatingCache;
33 import com.android.tv.util.ImageLoader;
34 import com.android.tv.util.Utils;
35 
36 import java.util.Arrays;
37 import java.util.Objects;
38 
39 /**
40  * A convenience class to create and insert program information entries into the database.
41  */
42 public final class Program implements Comparable<Program> {
43     private static final boolean DEBUG = false;
44     private static final boolean DEBUG_DUMP_DESCRIPTION = false;
45     private static final String TAG = "Program";
46 
47     private static final String[] PROJECTION_BASE = {
48             // Columns must match what is read in Program.fromCursor()
49             TvContract.Programs._ID,
50             TvContract.Programs.COLUMN_CHANNEL_ID,
51             TvContract.Programs.COLUMN_TITLE,
52             TvContract.Programs.COLUMN_EPISODE_TITLE,
53             TvContract.Programs.COLUMN_SHORT_DESCRIPTION,
54             TvContract.Programs.COLUMN_POSTER_ART_URI,
55             TvContract.Programs.COLUMN_THUMBNAIL_URI,
56             TvContract.Programs.COLUMN_CANONICAL_GENRE,
57             TvContract.Programs.COLUMN_CONTENT_RATING,
58             TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS,
59             TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS,
60             TvContract.Programs.COLUMN_VIDEO_WIDTH,
61             TvContract.Programs.COLUMN_VIDEO_HEIGHT
62     };
63 
64     // Columns which is deprecated in NYC
65     private static final String[] PROJECTION_DEPRECATED_IN_NYC = {
66             TvContract.Programs.COLUMN_SEASON_NUMBER,
67             TvContract.Programs.COLUMN_EPISODE_NUMBER
68     };
69 
70     private static final String[] PROJECTION_ADDED_IN_NYC = {
71             TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER,
72             TvContract.Programs.COLUMN_SEASON_TITLE,
73             TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER
74     };
75 
76     public static final String[] PROJECTION = createProjection();
77 
createProjection()78     private static String[] createProjection() {
79         return CollectionUtils
80                 .concatAll(PROJECTION_BASE, BuildCompat.isAtLeastN() ? PROJECTION_ADDED_IN_NYC
81                 : PROJECTION_DEPRECATED_IN_NYC);
82     }
83 
84     /**
85      * Creates {@code Program} object from cursor.
86      *
87      * <p>The query that created the cursor MUST use {@link #PROJECTION}.
88      */
fromCursor(Cursor cursor)89     public static Program fromCursor(Cursor cursor) {
90         // Columns read must match the order of match {@link #PROJECTION}
91         Builder builder = new Builder();
92         int index = 0;
93         builder.setId(cursor.getLong(index++));
94         builder.setChannelId(cursor.getLong(index++));
95         builder.setTitle(cursor.getString(index++));
96         builder.setEpisodeTitle(cursor.getString(index++));
97         builder.setDescription(cursor.getString(index++));
98         builder.setPosterArtUri(cursor.getString(index++));
99         builder.setThumbnailUri(cursor.getString(index++));
100         builder.setCanonicalGenres(cursor.getString(index++));
101         builder.setContentRatings(
102                 TvContentRatingCache.getInstance().getRatings(cursor.getString(index++)));
103         builder.setStartTimeUtcMillis(cursor.getLong(index++));
104         builder.setEndTimeUtcMillis(cursor.getLong(index++));
105         builder.setVideoWidth((int) cursor.getLong(index++));
106         builder.setVideoHeight((int) cursor.getLong(index++));
107         if (BuildCompat.isAtLeastN()) {
108             builder.setSeasonNumber(cursor.getString(index++));
109             builder.setSeasonTitle(cursor.getString(index++));
110             builder.setEpisodeNumber(cursor.getString(index++));
111         } else {
112             builder.setSeasonNumber(cursor.getString(index++));
113             builder.setEpisodeNumber(cursor.getString(index++));
114         }
115         return builder.build();
116     }
117 
118     private long mId;
119     private long mChannelId;
120     private String mTitle;
121     private String mEpisodeTitle;
122     private String mSeasonNumber;
123     private String mSeasonTitle;
124     private String mEpisodeNumber;
125     private long mStartTimeUtcMillis;
126     private long mEndTimeUtcMillis;
127     private String mDescription;
128     private int mVideoWidth;
129     private int mVideoHeight;
130     private String mPosterArtUri;
131     private String mThumbnailUri;
132     private int[] mCanonicalGenreIds;
133     private TvContentRating[] mContentRatings;
134 
135     /**
136      * TODO(DVR): Need to fill the following data.
137      */
138     private boolean mRecordable;
139     private boolean mRecordingScheduled;
140 
Program()141     private Program() {
142         // Do nothing.
143     }
144 
getId()145     public long getId() {
146         return mId;
147     }
148 
getChannelId()149     public long getChannelId() {
150         return mChannelId;
151     }
152 
153     /**
154      * Returns {@code true} if this program is valid or {@code false} otherwise.
155      */
isValid()156     public boolean isValid() {
157         return mChannelId >= 0;
158     }
159 
160     /**
161      * Returns {@code true} if the program is valid and {@code false} otherwise.
162      */
isValid(Program program)163     public static boolean isValid(Program program) {
164         return program != null && program.isValid();
165     }
166 
getTitle()167     public String getTitle() {
168         return mTitle;
169     }
170 
getEpisodeTitle()171     public String getEpisodeTitle() {
172         return mEpisodeTitle;
173     }
174 
getEpisodeDisplayTitle(Context context)175     public String getEpisodeDisplayTitle(Context context) {
176         if (!TextUtils.isEmpty(mSeasonNumber) && !TextUtils.isEmpty(mEpisodeNumber)
177                 && !TextUtils.isEmpty(mEpisodeTitle)) {
178             return String.format(context.getResources().getString(R.string.episode_format),
179                     mSeasonNumber, mEpisodeNumber, mEpisodeTitle);
180         }
181         return mEpisodeTitle;
182     }
183 
getSeasonNumber()184     public String getSeasonNumber() {
185         return mSeasonNumber;
186     }
187 
getEpisodeNumber()188     public String getEpisodeNumber() {
189         return mEpisodeNumber;
190     }
191 
getStartTimeUtcMillis()192     public long getStartTimeUtcMillis() {
193         return mStartTimeUtcMillis;
194     }
195 
getEndTimeUtcMillis()196     public long getEndTimeUtcMillis() {
197         return mEndTimeUtcMillis;
198     }
199 
200     /**
201      * Returns the program duration.
202      */
getDurationMillis()203     public long getDurationMillis() {
204         return mEndTimeUtcMillis - mStartTimeUtcMillis;
205     }
206 
getDescription()207     public String getDescription() {
208         return mDescription;
209     }
210 
getVideoWidth()211     public int getVideoWidth() {
212         return mVideoWidth;
213     }
214 
getVideoHeight()215     public int getVideoHeight() {
216         return mVideoHeight;
217     }
218 
getContentRatings()219     public TvContentRating[] getContentRatings() {
220         return mContentRatings;
221     }
222 
getPosterArtUri()223     public String getPosterArtUri() {
224         return mPosterArtUri;
225     }
226 
getThumbnailUri()227     public String getThumbnailUri() {
228         return mThumbnailUri;
229     }
230 
231     /**
232      * Returns array of canonical genres for this program.
233      * This is expected to be called rarely.
234      */
getCanonicalGenres()235     public String[] getCanonicalGenres() {
236         if (mCanonicalGenreIds == null) {
237             return null;
238         }
239         String[] genres = new String[mCanonicalGenreIds.length];
240         for (int i = 0; i < mCanonicalGenreIds.length; i++) {
241             genres[i] = GenreItems.getCanonicalGenre(mCanonicalGenreIds[i]);
242         }
243         return genres;
244     }
245 
246     /**
247      * Returns if this program has the genre.
248      */
hasGenre(int genreId)249     public boolean hasGenre(int genreId) {
250         if (genreId == GenreItems.ID_ALL_CHANNELS) {
251             return true;
252         }
253         if (mCanonicalGenreIds != null) {
254             for (int id : mCanonicalGenreIds) {
255                 if (id == genreId) {
256                     return true;
257                 }
258             }
259         }
260         return false;
261     }
262 
263     @Override
hashCode()264     public int hashCode() {
265         return Objects.hash(mChannelId, mStartTimeUtcMillis, mEndTimeUtcMillis,
266                 mTitle, mEpisodeTitle, mDescription, mVideoWidth, mVideoHeight,
267                 mPosterArtUri, mThumbnailUri, Arrays.hashCode(mContentRatings),
268                 Arrays.hashCode(mCanonicalGenreIds), mSeasonNumber, mSeasonTitle, mEpisodeNumber);
269     }
270 
271     @Override
equals(Object other)272     public boolean equals(Object other) {
273         if (!(other instanceof Program)) {
274             return false;
275         }
276         Program program = (Program) other;
277         return mChannelId == program.mChannelId
278                 && mStartTimeUtcMillis == program.mStartTimeUtcMillis
279                 && mEndTimeUtcMillis == program.mEndTimeUtcMillis
280                 && Objects.equals(mTitle, program.mTitle)
281                 && Objects.equals(mEpisodeTitle, program.mEpisodeTitle)
282                 && Objects.equals(mDescription, program.mDescription)
283                 && mVideoWidth == program.mVideoWidth
284                 && mVideoHeight == program.mVideoHeight
285                 && Objects.equals(mPosterArtUri, program.mPosterArtUri)
286                 && Objects.equals(mThumbnailUri, program.mThumbnailUri)
287                 && Arrays.equals(mContentRatings, program.mContentRatings)
288                 && Arrays.equals(mCanonicalGenreIds, program.mCanonicalGenreIds)
289                 && Objects.equals(mSeasonNumber, program.mSeasonNumber)
290                 && Objects.equals(mSeasonTitle, program.mSeasonTitle)
291                 && Objects.equals(mEpisodeNumber, program.mEpisodeNumber);
292     }
293 
294     @Override
compareTo(@onNull Program other)295     public int compareTo(@NonNull Program other) {
296         return Long.compare(mStartTimeUtcMillis, other.mStartTimeUtcMillis);
297     }
298 
299     @Override
toString()300     public String toString() {
301         StringBuilder builder = new StringBuilder();
302         builder.append("Program[" + mId + "]{")
303                 .append("channelId=").append(mChannelId)
304                 .append(", title=").append(mTitle)
305                 .append(", episodeTitle=").append(mEpisodeTitle)
306                 .append(", seasonNumber=").append(mSeasonNumber)
307                 .append(", seasonTitle=").append(mSeasonTitle)
308                 .append(", episodeNumber=").append(mEpisodeNumber)
309                 .append(", startTimeUtcSec=").append(Utils.toTimeString(mStartTimeUtcMillis))
310                 .append(", endTimeUtcSec=").append(Utils.toTimeString(mEndTimeUtcMillis))
311                 .append(", videoWidth=").append(mVideoWidth)
312                 .append(", videoHeight=").append(mVideoHeight)
313                 .append(", contentRatings=")
314                 .append(TvContentRatingCache.contentRatingsToString(mContentRatings))
315                 .append(", posterArtUri=").append(mPosterArtUri)
316                 .append(", thumbnailUri=").append(mThumbnailUri)
317                 .append(", canonicalGenres=").append(Arrays.toString(mCanonicalGenreIds));
318         if (DEBUG_DUMP_DESCRIPTION) {
319             builder.append(", description=").append(mDescription);
320         }
321         return builder.append("}").toString();
322     }
323 
copyFrom(Program other)324     public void copyFrom(Program other) {
325         if (this == other) {
326             return;
327         }
328 
329         mId = other.mId;
330         mChannelId = other.mChannelId;
331         mTitle = other.mTitle;
332         mEpisodeTitle = other.mEpisodeTitle;
333         mSeasonNumber = other.mSeasonNumber;
334         mSeasonTitle = other.mSeasonTitle;
335         mEpisodeNumber = other.mEpisodeNumber;
336         mStartTimeUtcMillis = other.mStartTimeUtcMillis;
337         mEndTimeUtcMillis = other.mEndTimeUtcMillis;
338         mDescription = other.mDescription;
339         mVideoWidth = other.mVideoWidth;
340         mVideoHeight = other.mVideoHeight;
341         mPosterArtUri = other.mPosterArtUri;
342         mThumbnailUri = other.mThumbnailUri;
343         mCanonicalGenreIds = other.mCanonicalGenreIds;
344         mContentRatings = other.mContentRatings;
345     }
346 
347     public static final class Builder {
348         private final Program mProgram;
349         private long mId;
350 
Builder()351         public Builder() {
352             mProgram = new Program();
353             // Fill initial data.
354             mProgram.mChannelId = Channel.INVALID_ID;
355             mProgram.mTitle = null;
356             mProgram.mSeasonNumber = null;
357             mProgram.mSeasonTitle = null;
358             mProgram.mEpisodeNumber = null;
359             mProgram.mStartTimeUtcMillis = -1;
360             mProgram.mEndTimeUtcMillis = -1;
361             mProgram.mDescription = null;
362         }
363 
Builder(Program other)364         public Builder(Program other) {
365             mProgram = new Program();
366             mProgram.copyFrom(other);
367         }
368 
setId(long id)369         public Builder setId(long id) {
370             mProgram.mId = id;
371             return this;
372         }
373 
setChannelId(long channelId)374         public Builder setChannelId(long channelId) {
375             mProgram.mChannelId = channelId;
376             return this;
377         }
378 
setTitle(String title)379         public Builder setTitle(String title) {
380             mProgram.mTitle = title;
381             return this;
382         }
383 
setEpisodeTitle(String episodeTitle)384         public Builder setEpisodeTitle(String episodeTitle) {
385             mProgram.mEpisodeTitle = episodeTitle;
386             return this;
387         }
388 
setSeasonNumber(String seasonNumber)389         public Builder setSeasonNumber(String seasonNumber) {
390             mProgram.mSeasonNumber = seasonNumber;
391             return this;
392         }
393 
setSeasonTitle(String seasonTitle)394         public Builder setSeasonTitle(String seasonTitle) {
395             mProgram.mSeasonTitle = seasonTitle;
396             return this;
397         }
398 
setEpisodeNumber(String episodeNumber)399         public Builder setEpisodeNumber(String episodeNumber) {
400             mProgram.mEpisodeNumber = episodeNumber;
401             return this;
402         }
403 
setStartTimeUtcMillis(long startTimeUtcMillis)404         public Builder setStartTimeUtcMillis(long startTimeUtcMillis) {
405             mProgram.mStartTimeUtcMillis = startTimeUtcMillis;
406             return this;
407         }
408 
setEndTimeUtcMillis(long endTimeUtcMillis)409         public Builder setEndTimeUtcMillis(long endTimeUtcMillis) {
410             mProgram.mEndTimeUtcMillis = endTimeUtcMillis;
411             return this;
412         }
413 
setDescription(String description)414         public Builder setDescription(String description) {
415             mProgram.mDescription = description;
416             return this;
417         }
418 
setVideoWidth(int width)419         public Builder setVideoWidth(int width) {
420             mProgram.mVideoWidth = width;
421             return this;
422         }
423 
setVideoHeight(int height)424         public Builder setVideoHeight(int height) {
425             mProgram.mVideoHeight = height;
426             return this;
427         }
428 
setContentRatings(TvContentRating[] contentRatings)429         public Builder setContentRatings(TvContentRating[] contentRatings) {
430             mProgram.mContentRatings = contentRatings;
431             return this;
432         }
433 
setPosterArtUri(String posterArtUri)434         public Builder setPosterArtUri(String posterArtUri) {
435             mProgram.mPosterArtUri = posterArtUri;
436             return this;
437         }
438 
setThumbnailUri(String thumbnailUri)439         public Builder setThumbnailUri(String thumbnailUri) {
440             mProgram.mThumbnailUri = thumbnailUri;
441             return this;
442         }
443 
setCanonicalGenres(String genres)444         public Builder setCanonicalGenres(String genres) {
445             if (TextUtils.isEmpty(genres)) {
446                 return this;
447             }
448             String[] canonicalGenres = TvContract.Programs.Genres.decode(genres);
449             if (canonicalGenres.length > 0) {
450                 int[] temp = new int[canonicalGenres.length];
451                 int i = 0;
452                 for (String canonicalGenre : canonicalGenres) {
453                     int genreId = GenreItems.getId(canonicalGenre);
454                     if (genreId == GenreItems.ID_ALL_CHANNELS) {
455                         // Skip if the genre is unknown.
456                         continue;
457                     }
458                     temp[i++] = genreId;
459                 }
460                 if (i < canonicalGenres.length) {
461                     temp = Arrays.copyOf(temp, i);
462                 }
463                 mProgram.mCanonicalGenreIds=temp;
464             }
465             return this;
466         }
467 
build()468         public Program build() {
469             Program program = new Program();
470             program.copyFrom(mProgram);
471             return program;
472         }
473     }
474 
475     /**
476      * Prefetches the program poster art.<p>
477      */
prefetchPosterArt(Context context, int posterArtWidth, int posterArtHeight)478     public void prefetchPosterArt(Context context, int posterArtWidth, int posterArtHeight) {
479         if (mPosterArtUri == null) {
480             return;
481         }
482         ImageLoader.prefetchBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight);
483     }
484 
485     /**
486      * Loads the program poster art and returns it via {@code callback}.<p>
487      * <p>
488      * Note that it may directly call {@code callback} if the program poster art already is loaded.
489      */
490     @UiThread
loadPosterArt(Context context, int posterArtWidth, int posterArtHeight, ImageLoader.ImageLoaderCallback callback)491     public void loadPosterArt(Context context, int posterArtWidth, int posterArtHeight,
492             ImageLoader.ImageLoaderCallback callback) {
493         if (mPosterArtUri == null) {
494             return;
495         }
496         ImageLoader.loadBitmap(context, mPosterArtUri, posterArtWidth, posterArtHeight, callback);
497     }
498 
isDuplicate(Program p1, Program p2)499     public static boolean isDuplicate(Program p1, Program p2) {
500         if (p1 == null || p2 == null) {
501             return false;
502         }
503         boolean isDuplicate = p1.getChannelId() == p2.getChannelId()
504                 && p1.getStartTimeUtcMillis() == p2.getStartTimeUtcMillis()
505                 && p1.getEndTimeUtcMillis() == p2.getEndTimeUtcMillis();
506         if (DEBUG && BuildConfig.ENG && isDuplicate) {
507             Log.w(TAG, "Duplicate programs detected! - \"" + p1.getTitle() + "\" and \""
508                     + p2.getTitle() + "\"");
509         }
510         return isDuplicate;
511     }
512 }
513