Home | History | Annotate | Download | only in data
      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 
     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