Program.java revision 369b6a409204a9b2a95f7ba575d7c3b7bdc94ab7
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.content.Context;
20import android.database.Cursor;
21import android.media.tv.TvContentRating;
22import android.media.tv.TvContract;
23import android.support.annotation.NonNull;
24import android.support.annotation.UiThread;
25import android.support.v4.os.BuildCompat;
26import android.text.TextUtils;
27import android.util.Log;
28
29import com.android.tv.R;
30import com.android.tv.common.BuildConfig;
31import com.android.tv.common.CollectionUtils;
32import com.android.tv.common.TvContentRatingCache;
33import com.android.tv.util.ImageLoader;
34import com.android.tv.util.Utils;
35
36import java.util.Arrays;
37import java.util.Objects;
38
39/**
40 * A convenience class to create and insert program information entries into the database.
41 */
42public 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
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     */
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
141    private Program() {
142        // Do nothing.
143    }
144
145    public long getId() {
146        return mId;
147    }
148
149    public long getChannelId() {
150        return mChannelId;
151    }
152
153    /**
154     * Returns {@code true} if this program is valid or {@code false} otherwise.
155     */
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     */
163    public static boolean isValid(Program program) {
164        return program != null && program.isValid();
165    }
166
167    public String getTitle() {
168        return mTitle;
169    }
170
171    public String getEpisodeTitle() {
172        return mEpisodeTitle;
173    }
174
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
184    public String getSeasonNumber() {
185        return mSeasonNumber;
186    }
187
188    public String getEpisodeNumber() {
189        return mEpisodeNumber;
190    }
191
192    public long getStartTimeUtcMillis() {
193        return mStartTimeUtcMillis;
194    }
195
196    public long getEndTimeUtcMillis() {
197        return mEndTimeUtcMillis;
198    }
199
200    /**
201     * Returns the program duration.
202     */
203    public long getDurationMillis() {
204        return mEndTimeUtcMillis - mStartTimeUtcMillis;
205    }
206
207    public String getDescription() {
208        return mDescription;
209    }
210
211    public int getVideoWidth() {
212        return mVideoWidth;
213    }
214
215    public int getVideoHeight() {
216        return mVideoHeight;
217    }
218
219    public TvContentRating[] getContentRatings() {
220        return mContentRatings;
221    }
222
223    public String getPosterArtUri() {
224        return mPosterArtUri;
225    }
226
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     */
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     */
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
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
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
295    public int compareTo(@NonNull Program other) {
296        return Long.compare(mStartTimeUtcMillis, other.mStartTimeUtcMillis);
297    }
298
299    @Override
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
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
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
364        public Builder(Program other) {
365            mProgram = new Program();
366            mProgram.copyFrom(other);
367        }
368
369        public Builder setId(long id) {
370            mProgram.mId = id;
371            return this;
372        }
373
374        public Builder setChannelId(long channelId) {
375            mProgram.mChannelId = channelId;
376            return this;
377        }
378
379        public Builder setTitle(String title) {
380            mProgram.mTitle = title;
381            return this;
382        }
383
384        public Builder setEpisodeTitle(String episodeTitle) {
385            mProgram.mEpisodeTitle = episodeTitle;
386            return this;
387        }
388
389        public Builder setSeasonNumber(String seasonNumber) {
390            mProgram.mSeasonNumber = seasonNumber;
391            return this;
392        }
393
394        public Builder setSeasonTitle(String seasonTitle) {
395            mProgram.mSeasonTitle = seasonTitle;
396            return this;
397        }
398
399        public Builder setEpisodeNumber(String episodeNumber) {
400            mProgram.mEpisodeNumber = episodeNumber;
401            return this;
402        }
403
404        public Builder setStartTimeUtcMillis(long startTimeUtcMillis) {
405            mProgram.mStartTimeUtcMillis = startTimeUtcMillis;
406            return this;
407        }
408
409        public Builder setEndTimeUtcMillis(long endTimeUtcMillis) {
410            mProgram.mEndTimeUtcMillis = endTimeUtcMillis;
411            return this;
412        }
413
414        public Builder setDescription(String description) {
415            mProgram.mDescription = description;
416            return this;
417        }
418
419        public Builder setVideoWidth(int width) {
420            mProgram.mVideoWidth = width;
421            return this;
422        }
423
424        public Builder setVideoHeight(int height) {
425            mProgram.mVideoHeight = height;
426            return this;
427        }
428
429        public Builder setContentRatings(TvContentRating[] contentRatings) {
430            mProgram.mContentRatings = contentRatings;
431            return this;
432        }
433
434        public Builder setPosterArtUri(String posterArtUri) {
435            mProgram.mPosterArtUri = posterArtUri;
436            return this;
437        }
438
439        public Builder setThumbnailUri(String thumbnailUri) {
440            mProgram.mThumbnailUri = thumbnailUri;
441            return this;
442        }
443
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
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     */
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
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
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