Initial commit for Tv Provider support lib.

Test: passes tests included in the change.
Bug: 34160270
Change-Id: Icc596198be2209ad0388658e47984745bf63c678
diff --git a/api/current.txt b/api/current.txt
index a4180b6..be25e5e 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -1024,6 +1024,351 @@
 
 }
 
+package android.support.media.tv {
+
+  public final class Channel {
+    method public static android.support.media.tv.Channel fromCursor(android.database.Cursor);
+    method public int getAppLinkColor();
+    method public java.lang.String getAppLinkIconUri();
+    method public java.lang.String getAppLinkIntentUri();
+    method public java.lang.String getAppLinkPosterArtUri();
+    method public java.lang.String getAppLinkText();
+    method public java.lang.String getChannelLogo();
+    method public java.lang.String getDescription();
+    method public java.lang.String getDisplayName();
+    method public java.lang.String getDisplayNumber();
+    method public long getId();
+    method public java.lang.String getInputId();
+    method public byte[] getInternalProviderDataByteArray();
+    method public java.lang.Long getInternalProviderFlag1();
+    method public java.lang.Long getInternalProviderFlag2();
+    method public java.lang.Long getInternalProviderFlag3();
+    method public java.lang.Long getInternalProviderFlag4();
+    method public java.lang.String getNetworkAffiliation();
+    method public int getOriginalNetworkId();
+    method public java.lang.String getPackageName();
+    method public int getServiceId();
+    method public java.lang.String getServiceType();
+    method public int getTransportStreamId();
+    method public java.lang.String getType();
+    method public java.lang.String getVideoFormat();
+    method public boolean isSearchable();
+    method public android.content.ContentValues toContentValues();
+  }
+
+  public static final class Channel.Builder {
+    ctor public Channel.Builder();
+    ctor public Channel.Builder(android.support.media.tv.Channel);
+    method public android.support.media.tv.Channel build();
+    method public android.support.media.tv.Channel.Builder setAppLinkColor(int);
+    method public android.support.media.tv.Channel.Builder setAppLinkIconUri(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setAppLinkIntent(android.content.Intent);
+    method public android.support.media.tv.Channel.Builder setAppLinkIntentUri(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setAppLinkPosterArtUri(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setAppLinkText(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setChannelLogo(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setDescription(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setDisplayName(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setDisplayNumber(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setInputId(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setInternalProviderData(byte[]);
+    method public android.support.media.tv.Channel.Builder setInternalProviderData(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setInternalProviderFlag1(long);
+    method public android.support.media.tv.Channel.Builder setInternalProviderFlag2(long);
+    method public android.support.media.tv.Channel.Builder setInternalProviderFlag3(long);
+    method public android.support.media.tv.Channel.Builder setInternalProviderFlag4(long);
+    method public android.support.media.tv.Channel.Builder setNetworkAffiliation(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setOriginalNetworkId(int);
+    method public android.support.media.tv.Channel.Builder setPackageName(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setSearchable(boolean);
+    method public android.support.media.tv.Channel.Builder setServiceId(int);
+    method public android.support.media.tv.Channel.Builder setServiceType(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setTransportStreamId(int);
+    method public android.support.media.tv.Channel.Builder setType(java.lang.String);
+    method public android.support.media.tv.Channel.Builder setVideoFormat(java.lang.String);
+  }
+
+  public final class Program implements java.lang.Comparable {
+    method public int compareTo(android.support.media.tv.Program);
+    method public static android.support.media.tv.Program fromCursor(android.database.Cursor);
+    method public java.lang.String[] getAudioLanguages();
+    method public java.lang.String[] getBroadcastGenres();
+    method public java.lang.String[] getCanonicalGenres();
+    method public long getChannelId();
+    method public android.media.tv.TvContentRating[] getContentRatings();
+    method public java.lang.String getDescription();
+    method public long getEndTimeUtcMillis();
+    method public java.lang.String getEpisodeNumber();
+    method public java.lang.String getEpisodeTitle();
+    method public long getId();
+    method public byte[] getInternalProviderDataByteArray();
+    method public java.lang.Long getInternalProviderFlag1();
+    method public java.lang.Long getInternalProviderFlag2();
+    method public java.lang.Long getInternalProviderFlag3();
+    method public java.lang.Long getInternalProviderFlag4();
+    method public java.lang.String getInternalProviderId();
+    method public java.lang.String getLongDescription();
+    method public java.lang.String getPosterArtUri();
+    method public java.lang.String getPreviewAppLinkIntentUri();
+    method public int getPreviewDuration();
+    method public int getPreviewLastPlaybackPosition();
+    method public java.lang.String getPreviewVideoUri();
+    method public int getPreviewWeight();
+    method public java.lang.String getSeasonNumber();
+    method public java.lang.String getSeasonTitle();
+    method public long getStartTimeUtcMillis();
+    method public java.lang.String getThumbnailUri();
+    method public java.lang.String getTitle();
+    method public int getVideoHeight();
+    method public int getVideoWidth();
+    method public boolean isRecordingProhibited();
+    method public boolean isSearchable();
+    method public android.content.ContentValues toContentValues();
+  }
+
+  public static final class Program.Builder {
+    ctor public Program.Builder();
+    ctor public Program.Builder(android.support.media.tv.Program);
+    method public android.support.media.tv.Program build();
+    method public android.support.media.tv.Program.Builder setAudioLanguages(java.lang.String[]);
+    method public android.support.media.tv.Program.Builder setBroadcastGenres(java.lang.String[]);
+    method public android.support.media.tv.Program.Builder setCanonicalGenres(java.lang.String[]);
+    method public android.support.media.tv.Program.Builder setChannelId(long);
+    method public android.support.media.tv.Program.Builder setContentRatings(android.media.tv.TvContentRating[]);
+    method public android.support.media.tv.Program.Builder setDescription(java.lang.String);
+    method public android.support.media.tv.Program.Builder setEndTimeUtcMillis(long);
+    method public android.support.media.tv.Program.Builder setEpisodeNumber(int);
+    method public android.support.media.tv.Program.Builder setEpisodeNumber(java.lang.String, int);
+    method public android.support.media.tv.Program.Builder setEpisodeTitle(java.lang.String);
+    method public android.support.media.tv.Program.Builder setId(long);
+    method public android.support.media.tv.Program.Builder setInternalProviderData(byte[]);
+    method public android.support.media.tv.Program.Builder setInternalProviderFlag1(long);
+    method public android.support.media.tv.Program.Builder setInternalProviderFlag2(long);
+    method public android.support.media.tv.Program.Builder setInternalProviderFlag3(long);
+    method public android.support.media.tv.Program.Builder setInternalProviderFlag4(long);
+    method public android.support.media.tv.Program.Builder setInternalProviderId(java.lang.String);
+    method public android.support.media.tv.Program.Builder setLongDescription(java.lang.String);
+    method public android.support.media.tv.Program.Builder setPosterArtUri(java.lang.String);
+    method public android.support.media.tv.Program.Builder setPreviewDuration(int);
+    method public android.support.media.tv.Program.Builder setPreviewIntentUri(java.lang.String);
+    method public android.support.media.tv.Program.Builder setPreviewLastPlaybackPosition(int);
+    method public android.support.media.tv.Program.Builder setPreviewVideoUri(java.lang.String);
+    method public android.support.media.tv.Program.Builder setPreviewWeight(int);
+    method public android.support.media.tv.Program.Builder setRecordingProhibited(boolean);
+    method public android.support.media.tv.Program.Builder setSearchable(boolean);
+    method public android.support.media.tv.Program.Builder setSeasonNumber(int);
+    method public android.support.media.tv.Program.Builder setSeasonNumber(java.lang.String, int);
+    method public android.support.media.tv.Program.Builder setSeasonTitle(java.lang.String);
+    method public android.support.media.tv.Program.Builder setStartTimeUtcMillis(long);
+    method public android.support.media.tv.Program.Builder setThumbnailUri(java.lang.String);
+    method public android.support.media.tv.Program.Builder setTitle(java.lang.String);
+    method public android.support.media.tv.Program.Builder setVideoHeight(int);
+    method public android.support.media.tv.Program.Builder setVideoWidth(int);
+  }
+
+  public final class TvContractCompat {
+    method public static android.net.Uri buildChannelLogoUri(long);
+    method public static android.net.Uri buildChannelLogoUri(android.net.Uri);
+    method public static android.net.Uri buildChannelUri(long);
+    method public static android.net.Uri buildChannelUriForPassthroughInput(java.lang.String);
+    method public static android.net.Uri buildChannelsUriForInput(java.lang.String);
+    method public static java.lang.String buildInputId(android.content.ComponentName);
+    method public static android.net.Uri buildProgramUri(long);
+    method public static android.net.Uri buildProgramsUriForChannel(long);
+    method public static android.net.Uri buildProgramsUriForChannel(android.net.Uri);
+    method public static android.net.Uri buildProgramsUriForChannel(long, long, long);
+    method public static android.net.Uri buildProgramsUriForChannel(android.net.Uri, long, long);
+    method public static android.net.Uri buildRecordedProgramUri(long);
+    method public static boolean isChannelUri(android.net.Uri);
+    method public static boolean isChannelUriForPassthroughInput(android.net.Uri);
+    method public static boolean isChannelUriForTunerInput(android.net.Uri);
+    method public static boolean isProgramUri(android.net.Uri);
+    field public static final java.lang.String AUTHORITY = "android.media.tv";
+  }
+
+  public static abstract interface TvContractCompat.BaseTvColumns {
+    field public static final java.lang.String COLUMN_PACKAGE_NAME = "package_name";
+  }
+
+  public static final class TvContractCompat.Channels implements android.support.media.tv.TvContractCompat.BaseTvColumns {
+    method public static java.lang.String getVideoResolution(java.lang.String);
+    field public static final java.lang.String COLUMN_APP_LINK_COLOR = "app_link_color";
+    field public static final java.lang.String COLUMN_APP_LINK_ICON_URI = "app_link_icon_uri";
+    field public static final java.lang.String COLUMN_APP_LINK_INTENT_URI = "app_link_intent_uri";
+    field public static final java.lang.String COLUMN_APP_LINK_POSTER_ART_URI = "app_link_poster_art_uri";
+    field public static final java.lang.String COLUMN_APP_LINK_TEXT = "app_link_text";
+    field public static final java.lang.String COLUMN_DESCRIPTION = "description";
+    field public static final java.lang.String COLUMN_DISPLAY_NAME = "display_name";
+    field public static final java.lang.String COLUMN_DISPLAY_NUMBER = "display_number";
+    field public static final java.lang.String COLUMN_INPUT_ID = "input_id";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG1 = "internal_provider_flag1";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG2 = "internal_provider_flag2";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG3 = "internal_provider_flag3";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG4 = "internal_provider_flag4";
+    field public static final java.lang.String COLUMN_NETWORK_AFFILIATION = "network_affiliation";
+    field public static final java.lang.String COLUMN_ORIGINAL_NETWORK_ID = "original_network_id";
+    field public static final java.lang.String COLUMN_SEARCHABLE = "searchable";
+    field public static final java.lang.String COLUMN_SERVICE_ID = "service_id";
+    field public static final java.lang.String COLUMN_SERVICE_TYPE = "service_type";
+    field public static final java.lang.String COLUMN_TRANSPORT_STREAM_ID = "transport_stream_id";
+    field public static final java.lang.String COLUMN_TYPE = "type";
+    field public static final java.lang.String COLUMN_VERSION_NUMBER = "version_number";
+    field public static final java.lang.String COLUMN_VIDEO_FORMAT = "video_format";
+    field public static final java.lang.String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/channel";
+    field public static final java.lang.String CONTENT_TYPE = "vnd.android.cursor.dir/channel";
+    field public static final android.net.Uri CONTENT_URI;
+    field public static final java.lang.String SERVICE_TYPE_AUDIO = "SERVICE_TYPE_AUDIO";
+    field public static final java.lang.String SERVICE_TYPE_AUDIO_VIDEO = "SERVICE_TYPE_AUDIO_VIDEO";
+    field public static final java.lang.String SERVICE_TYPE_OTHER = "SERVICE_TYPE_OTHER";
+    field public static final java.lang.String TYPE_1SEG = "TYPE_1SEG";
+    field public static final java.lang.String TYPE_ATSC_C = "TYPE_ATSC_C";
+    field public static final java.lang.String TYPE_ATSC_M_H = "TYPE_ATSC_M_H";
+    field public static final java.lang.String TYPE_ATSC_T = "TYPE_ATSC_T";
+    field public static final java.lang.String TYPE_CMMB = "TYPE_CMMB";
+    field public static final java.lang.String TYPE_DTMB = "TYPE_DTMB";
+    field public static final java.lang.String TYPE_DVB_C = "TYPE_DVB_C";
+    field public static final java.lang.String TYPE_DVB_C2 = "TYPE_DVB_C2";
+    field public static final java.lang.String TYPE_DVB_H = "TYPE_DVB_H";
+    field public static final java.lang.String TYPE_DVB_S = "TYPE_DVB_S";
+    field public static final java.lang.String TYPE_DVB_S2 = "TYPE_DVB_S2";
+    field public static final java.lang.String TYPE_DVB_SH = "TYPE_DVB_SH";
+    field public static final java.lang.String TYPE_DVB_T = "TYPE_DVB_T";
+    field public static final java.lang.String TYPE_DVB_T2 = "TYPE_DVB_T2";
+    field public static final java.lang.String TYPE_ISDB_C = "TYPE_ISDB_C";
+    field public static final java.lang.String TYPE_ISDB_S = "TYPE_ISDB_S";
+    field public static final java.lang.String TYPE_ISDB_T = "TYPE_ISDB_T";
+    field public static final java.lang.String TYPE_ISDB_TB = "TYPE_ISDB_TB";
+    field public static final java.lang.String TYPE_NTSC = "TYPE_NTSC";
+    field public static final java.lang.String TYPE_OTHER = "TYPE_OTHER";
+    field public static final java.lang.String TYPE_PAL = "TYPE_PAL";
+    field public static final java.lang.String TYPE_PREVIEW = "TYPE_PREVIEW";
+    field public static final java.lang.String TYPE_SECAM = "TYPE_SECAM";
+    field public static final java.lang.String TYPE_S_DMB = "TYPE_S_DMB";
+    field public static final java.lang.String TYPE_T_DMB = "TYPE_T_DMB";
+    field public static final java.lang.String VIDEO_FORMAT_1080I = "VIDEO_FORMAT_1080I";
+    field public static final java.lang.String VIDEO_FORMAT_1080P = "VIDEO_FORMAT_1080P";
+    field public static final java.lang.String VIDEO_FORMAT_2160P = "VIDEO_FORMAT_2160P";
+    field public static final java.lang.String VIDEO_FORMAT_240P = "VIDEO_FORMAT_240P";
+    field public static final java.lang.String VIDEO_FORMAT_360P = "VIDEO_FORMAT_360P";
+    field public static final java.lang.String VIDEO_FORMAT_4320P = "VIDEO_FORMAT_4320P";
+    field public static final java.lang.String VIDEO_FORMAT_480I = "VIDEO_FORMAT_480I";
+    field public static final java.lang.String VIDEO_FORMAT_480P = "VIDEO_FORMAT_480P";
+    field public static final java.lang.String VIDEO_FORMAT_576I = "VIDEO_FORMAT_576I";
+    field public static final java.lang.String VIDEO_FORMAT_576P = "VIDEO_FORMAT_576P";
+    field public static final java.lang.String VIDEO_FORMAT_720P = "VIDEO_FORMAT_720P";
+    field public static final java.lang.String VIDEO_RESOLUTION_ED = "VIDEO_RESOLUTION_ED";
+    field public static final java.lang.String VIDEO_RESOLUTION_FHD = "VIDEO_RESOLUTION_FHD";
+    field public static final java.lang.String VIDEO_RESOLUTION_HD = "VIDEO_RESOLUTION_HD";
+    field public static final java.lang.String VIDEO_RESOLUTION_SD = "VIDEO_RESOLUTION_SD";
+    field public static final java.lang.String VIDEO_RESOLUTION_UHD = "VIDEO_RESOLUTION_UHD";
+  }
+
+  public static final class TvContractCompat.Channels.Logo {
+    field public static final java.lang.String CONTENT_DIRECTORY = "logo";
+  }
+
+  public static final class TvContractCompat.Programs implements android.support.media.tv.TvContractCompat.BaseTvColumns {
+    field public static final java.lang.String COLUMN_AUDIO_LANGUAGE = "audio_language";
+    field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre";
+    field public static final java.lang.String COLUMN_CANONICAL_GENRE = "canonical_genre";
+    field public static final java.lang.String COLUMN_CHANNEL_ID = "channel_id";
+    field public static final java.lang.String COLUMN_CONTENT_RATING = "content_rating";
+    field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
+    field public static final java.lang.String COLUMN_EPISODE_DISPLAY_NUMBER = "episode_display_number";
+    field public static final deprecated java.lang.String COLUMN_EPISODE_NUMBER = "episode_number";
+    field public static final java.lang.String COLUMN_EPISODE_TITLE = "episode_title";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG1 = "internal_provider_flag1";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG2 = "internal_provider_flag2";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG3 = "internal_provider_flag3";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG4 = "internal_provider_flag4";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_ID = "internal_provider_id";
+    field public static final java.lang.String COLUMN_LONG_DESCRIPTION = "long_description";
+    field public static final java.lang.String COLUMN_POSTER_ART_URI = "poster_art_uri";
+    field public static final java.lang.String COLUMN_PREVIEW_DURATION = "preview_duration";
+    field public static final java.lang.String COLUMN_PREVIEW_INTENT_URI = "preview_intent_uri";
+    field public static final java.lang.String COLUMN_PREVIEW_LAST_PLAYBACK_POSITION = "preview_last_playback_position";
+    field public static final java.lang.String COLUMN_PREVIEW_VIDEO_URI = "preview_video_uri";
+    field public static final java.lang.String COLUMN_PREVIEW_WEIGHT = "preview_weight";
+    field public static final java.lang.String COLUMN_RECORDING_PROHIBITED = "recording_prohibited";
+    field public static final java.lang.String COLUMN_SEARCHABLE = "searchable";
+    field public static final java.lang.String COLUMN_SEASON_DISPLAY_NUMBER = "season_display_number";
+    field public static final deprecated java.lang.String COLUMN_SEASON_NUMBER = "season_number";
+    field public static final java.lang.String COLUMN_SEASON_TITLE = "season_title";
+    field public static final java.lang.String COLUMN_SHORT_DESCRIPTION = "short_description";
+    field public static final java.lang.String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis";
+    field public static final java.lang.String COLUMN_THUMBNAIL_URI = "thumbnail_uri";
+    field public static final java.lang.String COLUMN_TITLE = "title";
+    field public static final java.lang.String COLUMN_VERSION_NUMBER = "version_number";
+    field public static final java.lang.String COLUMN_VIDEO_HEIGHT = "video_height";
+    field public static final java.lang.String COLUMN_VIDEO_WIDTH = "video_width";
+    field public static final java.lang.String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/program";
+    field public static final java.lang.String CONTENT_TYPE = "vnd.android.cursor.dir/program";
+    field public static final android.net.Uri CONTENT_URI;
+  }
+
+  public static final class TvContractCompat.Programs.Genres {
+    method public static java.lang.String[] decode(java.lang.String);
+    method public static java.lang.String encode(java.lang.String...);
+    method public static boolean isCanonical(java.lang.String);
+    field public static final java.lang.String ANIMAL_WILDLIFE = "ANIMAL_WILDLIFE";
+    field public static final java.lang.String ARTS = "ARTS";
+    field public static final java.lang.String COMEDY = "COMEDY";
+    field public static final java.lang.String DRAMA = "DRAMA";
+    field public static final java.lang.String EDUCATION = "EDUCATION";
+    field public static final java.lang.String ENTERTAINMENT = "ENTERTAINMENT";
+    field public static final java.lang.String FAMILY_KIDS = "FAMILY_KIDS";
+    field public static final java.lang.String GAMING = "GAMING";
+    field public static final java.lang.String LIFE_STYLE = "LIFE_STYLE";
+    field public static final java.lang.String MOVIES = "MOVIES";
+    field public static final java.lang.String MUSIC = "MUSIC";
+    field public static final java.lang.String NEWS = "NEWS";
+    field public static final java.lang.String PREMIER = "PREMIER";
+    field public static final java.lang.String SHOPPING = "SHOPPING";
+    field public static final java.lang.String SPORTS = "SPORTS";
+    field public static final java.lang.String TECH_SCIENCE = "TECH_SCIENCE";
+    field public static final java.lang.String TRAVEL = "TRAVEL";
+  }
+
+  public static final class TvContractCompat.RecordedPrograms implements android.support.media.tv.TvContractCompat.BaseTvColumns {
+    field public static final java.lang.String COLUMN_AUDIO_LANGUAGE = "audio_language";
+    field public static final java.lang.String COLUMN_BROADCAST_GENRE = "broadcast_genre";
+    field public static final java.lang.String COLUMN_CANONICAL_GENRE = "canonical_genre";
+    field public static final java.lang.String COLUMN_CHANNEL_ID = "channel_id";
+    field public static final java.lang.String COLUMN_CONTENT_RATING = "content_rating";
+    field public static final java.lang.String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
+    field public static final java.lang.String COLUMN_EPISODE_DISPLAY_NUMBER = "episode_display_number";
+    field public static final java.lang.String COLUMN_EPISODE_TITLE = "episode_title";
+    field public static final java.lang.String COLUMN_INPUT_ID = "input_id";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG1 = "internal_provider_flag1";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG2 = "internal_provider_flag2";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG3 = "internal_provider_flag3";
+    field public static final java.lang.String COLUMN_INTERNAL_PROVIDER_FLAG4 = "internal_provider_flag4";
+    field public static final java.lang.String COLUMN_LONG_DESCRIPTION = "long_description";
+    field public static final java.lang.String COLUMN_POSTER_ART_URI = "poster_art_uri";
+    field public static final java.lang.String COLUMN_RECORDING_DATA_BYTES = "recording_data_bytes";
+    field public static final java.lang.String COLUMN_RECORDING_DATA_URI = "recording_data_uri";
+    field public static final java.lang.String COLUMN_RECORDING_DURATION_MILLIS = "recording_duration_millis";
+    field public static final java.lang.String COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS = "recording_expire_time_utc_millis";
+    field public static final java.lang.String COLUMN_SEARCHABLE = "searchable";
+    field public static final java.lang.String COLUMN_SEASON_DISPLAY_NUMBER = "season_display_number";
+    field public static final java.lang.String COLUMN_SEASON_TITLE = "season_title";
+    field public static final java.lang.String COLUMN_SHORT_DESCRIPTION = "short_description";
+    field public static final java.lang.String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis";
+    field public static final java.lang.String COLUMN_THUMBNAIL_URI = "thumbnail_uri";
+    field public static final java.lang.String COLUMN_TITLE = "title";
+    field public static final java.lang.String COLUMN_VERSION_NUMBER = "version_number";
+    field public static final java.lang.String COLUMN_VIDEO_HEIGHT = "video_height";
+    field public static final java.lang.String COLUMN_VIDEO_WIDTH = "video_width";
+    field public static final java.lang.String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/recorded_program";
+    field public static final java.lang.String CONTENT_TYPE = "vnd.android.cursor.dir/recorded_program";
+    field public static final android.net.Uri CONTENT_URI;
+  }
+
+}
+
 package android.support.percent {
 
   public class PercentFrameLayout extends android.widget.FrameLayout {
diff --git a/settings.gradle b/settings.gradle
index b217355..addb7a1 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -88,6 +88,9 @@
 include ':support-wearable'
 project(':support-wearable').projectDir = new File(rootDir, 'wearable')
 
+include ':support-tv-provider'
+project(':support-tv-provider').projectDir = new File(rootDir, 'tv-provider')
+
 /////////////////////////////
 //
 // Samples
diff --git a/tv-provider/Android.mk b/tv-provider/Android.mk
new file mode 100644
index 0000000..4fa8af4
--- /dev/null
+++ b/tv-provider/Android.mk
@@ -0,0 +1,34 @@
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+# Here is the final static library that apps can link against.
+# Applications that use this library must include it with
+#
+#   LOCAL_STATIC_ANDROID_LIBRARIES := android-support-tv-provider
+#
+include $(CLEAR_VARS)
+LOCAL_USE_AAPT2 := true
+LOCAL_MODULE := android-support-tv-provider
+LOCAL_SDK_VERSION := $(SUPPORT_CURRENT_SDK_VERSION)
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_SHARED_ANDROID_LIBRARIES := \
+    android-support-compat \
+    android-support-annotations
+LOCAL_JAR_EXCLUDE_FILES := none
+LOCAL_JAVA_LANGUAGE_VERSION := 1.7
+LOCAL_AAPT_FLAGS := --add-javadoc-annotation doconly
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tv-provider/AndroidManifest.xml b/tv-provider/AndroidManifest.xml
new file mode 100644
index 0000000..e479417
--- /dev/null
+++ b/tv-provider/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="android.support.media.tv">
+    <uses-sdk android:minSdkVersion="21"/>
+    <meta-data android:name="android.support.VERSION" android:value="${support-version}" />
+    <application />
+</manifest>
diff --git a/tv-provider/build.gradle b/tv-provider/build.gradle
new file mode 100644
index 0000000..ec384f6
--- /dev/null
+++ b/tv-provider/build.gradle
@@ -0,0 +1,57 @@
+apply plugin: android.support.SupportLibraryPlugin
+archivesBaseName = 'support-tv-provider'
+
+dependencies {
+    compile project(':support-annotations')
+    compile project(':support-compat')
+    androidTestCompile (libs.test_runner) {
+        exclude module: 'support-annotations'
+    }
+}
+
+android {
+    compileSdkVersion project.ext.currentSdk
+
+    defaultConfig {
+        minSdkVersion 21
+    }
+
+    sourceSets {
+        main.java.srcDirs = ['src']
+        main.res.srcDir 'res'
+    }
+}
+
+uploadArchives {
+    repositories {
+        mavenDeployer {
+            repository(url: uri(rootProject.ext.supportRepoOut)) {
+            }
+
+            pom.project {
+                name 'Android Support TV Provider'
+                description "Android Support Library for TV Provider"
+                url 'http://developer.android.com/tools/extras/support-library.html'
+                inceptionYear '2017'
+
+                licenses {
+                    license {
+                        name 'The Apache Software License, Version 2.0'
+                        url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+                        distribution 'repo'
+                    }
+                }
+
+                scm {
+                    url "http://source.android.com"
+                    connection "scm:git:https://android.googlesource.com/platform/frameworks/support"
+                }
+                developers {
+                    developer {
+                        name 'The Android Open Source Project'
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/tv-provider/src/android/support/media/tv/Channel.java b/tv-provider/src/android/support/media/tv/Channel.java
new file mode 100644
index 0000000..7a7559c
--- /dev/null
+++ b/tv-provider/src/android/support/media/tv/Channel.java
@@ -0,0 +1,951 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.tv;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Build;
+import android.support.annotation.RestrictTo;
+import android.support.media.tv.TvContractCompat.Channels;
+import android.support.v4.os.BuildCompat;
+import android.text.TextUtils;
+
+/**
+ * A convenience class to create and insert channel entries into the database.
+ */
+public final class Channel {
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String[] PROJECTION = getProjection();
+
+    private static final long INVALID_CHANNEL_ID = -1;
+    private static final int INVALID_INTEGER_VALUE = -1;
+    private static final int IS_SEARCHABLE = 1;
+    private static final int IS_TRANSIENT = 1;
+
+    private final long mId;
+    private final String mPackageName;
+    private final String mInputId;
+    private final String mType;
+    private final String mDisplayNumber;
+    private final String mDisplayName;
+    private final String mDescription;
+    private final String mChannelLogo;
+    private final String mVideoFormat;
+    private final int mOriginalNetworkId;
+    private final int mTransportStreamId;
+    private final int mServiceId;
+    private final String mAppLinkText;
+    private final int mAppLinkColor;
+    private final String mAppLinkIconUri;
+    private final String mAppLinkPosterArtUri;
+    private final String mAppLinkIntentUri;
+    private final byte[] mInternalProviderData;
+    private final String mNetworkAffiliation;
+    private final int mSearchable;
+    private final String mServiceType;
+    private final Long mInternalProviderFlag1;
+    private final Long mInternalProviderFlag2;
+    private final Long mInternalProviderFlag3;
+    private final Long mInternalProviderFlag4;
+    private final int mTransient;
+
+    private Channel(Builder builder) {
+        mId = builder.mId;
+        mPackageName = builder.mPackageName;
+        mInputId = builder.mInputId;
+        mType = builder.mType;
+        mDisplayNumber = builder.mDisplayNumber;
+        mDisplayName = builder.mDisplayName;
+        mDescription = builder.mDescription;
+        mVideoFormat = builder.mVideoFormat;
+        mOriginalNetworkId = builder.mOriginalNetworkId;
+        mTransportStreamId = builder.mTransportStreamId;
+        mServiceId = builder.mServiceId;
+        mAppLinkText = builder.mAppLinkText;
+        mAppLinkColor = builder.mAppLinkColor;
+        mAppLinkIconUri = builder.mAppLinkIconUri;
+        mAppLinkPosterArtUri = builder.mAppLinkPosterArtUri;
+        mAppLinkIntentUri = builder.mAppLinkIntentUri;
+        mChannelLogo = builder.mChannelLogo;
+        mInternalProviderData = builder.mInternalProviderData;
+        mNetworkAffiliation = builder.mNetworkAffiliation;
+        mSearchable = builder.mSearchable;
+        mServiceType = builder.mServiceType;
+        mInternalProviderFlag1 = builder.mInternalProviderFlag1;
+        mInternalProviderFlag2 = builder.mInternalProviderFlag2;
+        mInternalProviderFlag3 = builder.mInternalProviderFlag3;
+        mInternalProviderFlag4 = builder.mInternalProviderFlag4;
+        mTransient = builder.mTransient;
+    }
+
+    /**
+     * @return The value of {@link Channels#_ID} for the channel.
+     */
+    public long getId() {
+        return mId;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_PACKAGE_NAME} for the channel.
+     */
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_INPUT_ID} for the channel.
+     */
+    public String getInputId() {
+        return mInputId;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_TYPE} for the channel.
+     */
+    public String getType() {
+        return mType;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_DISPLAY_NUMBER} for the channel.
+     */
+    public String getDisplayNumber() {
+        return mDisplayNumber;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_DISPLAY_NAME} for the channel.
+     */
+    public String getDisplayName() {
+        return mDisplayName;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_DESCRIPTION} for the channel.
+     */
+    public String getDescription() {
+        return mDescription;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_VIDEO_FORMAT} for the channel.
+     */
+    public String getVideoFormat() {
+        return mVideoFormat;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_ORIGINAL_NETWORK_ID} for the channel.
+     */
+    public int getOriginalNetworkId() {
+        return mOriginalNetworkId;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_TRANSPORT_STREAM_ID} for the channel.
+     */
+    public int getTransportStreamId() {
+        return mTransportStreamId;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_SERVICE_ID} for the channel.
+     */
+    public int getServiceId() {
+        return mServiceId;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_APP_LINK_TEXT} for the channel.
+     */
+    public String getAppLinkText() {
+        return mAppLinkText;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_APP_LINK_COLOR} for the channel.
+     */
+    public int getAppLinkColor() {
+        return mAppLinkColor;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_APP_LINK_ICON_URI} for the channel.
+     */
+    public String getAppLinkIconUri() {
+        return mAppLinkIconUri;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_APP_LINK_POSTER_ART_URI} for the channel.
+     */
+    public String getAppLinkPosterArtUri() {
+        return mAppLinkPosterArtUri;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_APP_LINK_INTENT_URI} for the channel.
+     */
+    public String getAppLinkIntentUri() {
+        return mAppLinkIntentUri;
+    }
+
+    /**
+     * @return The value of {@link Channels.Logo} for the channel.
+     */
+    public String getChannelLogo() {
+        return mChannelLogo;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_NETWORK_AFFILIATION} for the channel.
+     */
+    public String getNetworkAffiliation() {
+        return mNetworkAffiliation;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_SEARCHABLE} for the channel.
+     */
+    public boolean isSearchable() {
+        return mSearchable == IS_SEARCHABLE;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_DATA} for the channel.
+     */
+    public byte[] getInternalProviderDataByteArray() {
+        return mInternalProviderData;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_SERVICE_TYPE} for the channel.
+     *
+     * <p>Returns {@link Channels#SERVICE_TYPE_AUDIO}, {@link Channels#SERVICE_TYPE_AUDIO_VIDEO}, or
+     * {@link Channels#SERVICE_TYPE_OTHER}.
+     */
+    public String getServiceType() {
+        return mServiceType;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG1} for the channel.
+     */
+    public Long getInternalProviderFlag1() {
+        return mInternalProviderFlag1;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG2} for the channel.
+     */
+    public Long getInternalProviderFlag2() {
+        return mInternalProviderFlag2;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG3} for the channel.
+     */
+    public Long getInternalProviderFlag3() {
+        return mInternalProviderFlag3;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG4} for the channel.
+     */
+    public Long getInternalProviderFlag4() {
+        return mInternalProviderFlag4;
+    }
+
+    /**
+     * @return The value of {@link Channels#COLUMN_TRANSIENT} for the channel.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public boolean isTransient() {
+        return mTransient == IS_TRANSIENT;
+    }
+
+    @Override
+    public String toString() {
+        return "Channel{"
+                + "id=" + mId
+                + ", packageName=" + mPackageName
+                + ", inputId=" + mInputId
+                + ", originalNetworkId=" + mOriginalNetworkId
+                + ", type=" + mType
+                + ", displayNumber=" + mDisplayNumber
+                + ", displayName=" + mDisplayName
+                + ", description=" + mDescription
+                + ", channelLogo=" + mChannelLogo
+                + ", videoFormat=" + mVideoFormat
+                + ", appLinkText=" + mAppLinkText + "}";
+    }
+
+    /**
+     * @return The fields of the Channel in the ContentValues format to be easily inserted into the
+     * TV Input Framework database.
+     */
+    public ContentValues toContentValues() {
+        ContentValues values = new ContentValues();
+        if (mId != INVALID_CHANNEL_ID) {
+            values.put(Channels._ID, mId);
+        }
+        if (!TextUtils.isEmpty(mPackageName)) {
+            values.put(Channels.COLUMN_PACKAGE_NAME, mPackageName);
+        } else {
+            values.putNull(Channels.COLUMN_PACKAGE_NAME);
+        }
+        if (!TextUtils.isEmpty(mInputId)) {
+            values.put(Channels.COLUMN_INPUT_ID, mInputId);
+        } else {
+            values.putNull(Channels.COLUMN_INPUT_ID);
+        }
+        if (!TextUtils.isEmpty(mType)) {
+            values.put(Channels.COLUMN_TYPE, mType);
+        } else {
+            values.putNull(Channels.COLUMN_TYPE);
+        }
+        if (!TextUtils.isEmpty(mDisplayNumber)) {
+            values.put(Channels.COLUMN_DISPLAY_NUMBER, mDisplayNumber);
+        } else {
+            values.putNull(Channels.COLUMN_DISPLAY_NUMBER);
+        }
+        if (!TextUtils.isEmpty(mDisplayName)) {
+            values.put(Channels.COLUMN_DISPLAY_NAME, mDisplayName);
+        } else {
+            values.putNull(Channels.COLUMN_DISPLAY_NAME);
+        }
+        if (!TextUtils.isEmpty(mDescription)) {
+            values.put(Channels.COLUMN_DESCRIPTION, mDescription);
+        } else {
+            values.putNull(Channels.COLUMN_DESCRIPTION);
+        }
+        if (!TextUtils.isEmpty(mVideoFormat)) {
+            values.put(Channels.COLUMN_VIDEO_FORMAT, mVideoFormat);
+        } else {
+            values.putNull(Channels.COLUMN_VIDEO_FORMAT);
+        }
+        if (mInternalProviderData != null && mInternalProviderData.length > 0) {
+            values.put(Channels.COLUMN_INTERNAL_PROVIDER_DATA,
+                    mInternalProviderData);
+        } else {
+            values.putNull(Channels.COLUMN_INTERNAL_PROVIDER_DATA);
+        }
+        values.put(Channels.COLUMN_ORIGINAL_NETWORK_ID, mOriginalNetworkId);
+        values.put(Channels.COLUMN_TRANSPORT_STREAM_ID, mTransportStreamId);
+        values.put(Channels.COLUMN_SERVICE_ID, mServiceId);
+        values.put(Channels.COLUMN_NETWORK_AFFILIATION, mNetworkAffiliation);
+        values.put(Channels.COLUMN_SEARCHABLE, mSearchable);
+        values.put(Channels.COLUMN_SERVICE_TYPE, mServiceType);
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            values.put(Channels.COLUMN_APP_LINK_COLOR, mAppLinkColor);
+            if (!TextUtils.isEmpty(mAppLinkText)) {
+                values.put(Channels.COLUMN_APP_LINK_TEXT, mAppLinkText);
+            } else {
+                values.putNull(Channels.COLUMN_APP_LINK_TEXT);
+            }
+            if (!TextUtils.isEmpty(mAppLinkIconUri)) {
+                values.put(Channels.COLUMN_APP_LINK_ICON_URI, mAppLinkIconUri);
+            } else {
+                values.putNull(Channels.COLUMN_APP_LINK_ICON_URI);
+            }
+            if (!TextUtils.isEmpty(mAppLinkPosterArtUri)) {
+                values.put(Channels.COLUMN_APP_LINK_POSTER_ART_URI,
+                        mAppLinkPosterArtUri);
+            } else {
+                values.putNull(Channels.COLUMN_APP_LINK_POSTER_ART_URI);
+            }
+            if (!TextUtils.isEmpty(mAppLinkIntentUri)) {
+                values.put(Channels.COLUMN_APP_LINK_INTENT_URI, mAppLinkIntentUri);
+            } else {
+                values.putNull(Channels.COLUMN_APP_LINK_INTENT_URI);
+            }
+            if (mInternalProviderFlag1 != null) {
+                values.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, mInternalProviderFlag1);
+            }
+            if (mInternalProviderFlag2 != null) {
+                values.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2, mInternalProviderFlag2);
+            }
+            if (mInternalProviderFlag3 != null) {
+                values.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3, mInternalProviderFlag3);
+            }
+            if (mInternalProviderFlag4 != null) {
+                values.put(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4, mInternalProviderFlag4);
+            }
+        }
+        if (BuildCompat.isAtLeastO()) {
+            values.put(Channels.COLUMN_TRANSIENT, mTransient);
+        }
+        return values;
+    }
+
+    /**
+     * Creates a Channel object from a cursor including the fields defined in {@link Channels}.
+     *
+     * @param cursor A row from the TV Input Framework database.
+     * @return A channel with the values taken from the cursor.
+     */
+    public static Channel fromCursor(Cursor cursor) {
+        // TODO: Add additional API which does not use costly getColumnIndex().
+        Builder builder = new Builder();
+        int index;
+        if ((index = cursor.getColumnIndex(Channels._ID)) >= 0 && !cursor.isNull(index)) {
+            builder.setId(cursor.getLong(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_DESCRIPTION)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setDescription(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_DISPLAY_NAME)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setDisplayName(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_DISPLAY_NUMBER)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setDisplayNumber(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_INPUT_ID)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setInputId(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_INTERNAL_PROVIDER_DATA)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setInternalProviderData(cursor.getBlob(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_NETWORK_AFFILIATION)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setNetworkAffiliation(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_ORIGINAL_NETWORK_ID)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setOriginalNetworkId(cursor.getInt(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_PACKAGE_NAME)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setPackageName(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_SEARCHABLE)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setSearchable(cursor.getInt(index) == IS_SEARCHABLE);
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_SERVICE_ID)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setServiceId(cursor.getInt(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_SERVICE_TYPE)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setServiceType(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_TRANSPORT_STREAM_ID)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setTransportStreamId(cursor.getInt(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_TYPE)) >= 0 && !cursor.isNull(index)) {
+            builder.setType(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Channels.COLUMN_VIDEO_FORMAT)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setVideoFormat(cursor.getString(index));
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            if ((index = cursor.getColumnIndex(Channels.COLUMN_APP_LINK_COLOR)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setAppLinkColor(cursor.getInt(index));
+            }
+            if ((index = cursor.getColumnIndex(Channels.COLUMN_APP_LINK_ICON_URI)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setAppLinkIconUri(cursor.getString(index));
+            }
+            if ((index = cursor.getColumnIndex(Channels.COLUMN_APP_LINK_INTENT_URI)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setAppLinkIntentUri(cursor.getString(index));
+            }
+            if ((index = cursor.getColumnIndex(Channels.COLUMN_APP_LINK_POSTER_ART_URI)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setAppLinkPosterArtUri(cursor.getString(index));
+            }
+            if ((index = cursor.getColumnIndex(Channels.COLUMN_APP_LINK_TEXT)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setAppLinkText(cursor.getString(index));
+            }
+            if ((index = cursor.getColumnIndex(Channels.COLUMN_INTERNAL_PROVIDER_FLAG1)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setInternalProviderFlag1(cursor.getLong(index));
+            }
+            if ((index = cursor.getColumnIndex(Channels.COLUMN_INTERNAL_PROVIDER_FLAG2)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setInternalProviderFlag2(cursor.getLong(index));
+            }
+            if ((index = cursor.getColumnIndex(Channels.COLUMN_INTERNAL_PROVIDER_FLAG3)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setInternalProviderFlag3(cursor.getLong(index));
+            }
+            if ((index = cursor.getColumnIndex(Channels.COLUMN_INTERNAL_PROVIDER_FLAG4)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setInternalProviderFlag4(cursor.getLong(index));
+            }
+        }
+        if (BuildCompat.isAtLeastO()) {
+            if ((index = cursor.getColumnIndex(Channels.COLUMN_TRANSIENT)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setTransient(cursor.getInt(index) == IS_TRANSIENT);
+            }
+        }
+        return builder.build();
+    }
+
+    private static String[] getProjection() {
+        String[] baseColumns = new String[] {
+                Channels._ID,
+                Channels.COLUMN_DESCRIPTION,
+                Channels.COLUMN_DISPLAY_NAME,
+                Channels.COLUMN_DISPLAY_NUMBER,
+                Channels.COLUMN_INPUT_ID,
+                Channels.COLUMN_INTERNAL_PROVIDER_DATA,
+                Channels.COLUMN_NETWORK_AFFILIATION,
+                Channels.COLUMN_ORIGINAL_NETWORK_ID,
+                Channels.COLUMN_PACKAGE_NAME,
+                Channels.COLUMN_SEARCHABLE,
+                Channels.COLUMN_SERVICE_ID,
+                Channels.COLUMN_SERVICE_TYPE,
+                Channels.COLUMN_TRANSPORT_STREAM_ID,
+                Channels.COLUMN_TYPE,
+                Channels.COLUMN_VIDEO_FORMAT,
+        };
+        String[] marshmallowColumns = new String[] {
+                Channels.COLUMN_APP_LINK_COLOR,
+                Channels.COLUMN_APP_LINK_ICON_URI,
+                Channels.COLUMN_APP_LINK_INTENT_URI,
+                Channels.COLUMN_APP_LINK_POSTER_ART_URI,
+                Channels.COLUMN_APP_LINK_TEXT,
+                Channels.COLUMN_INTERNAL_PROVIDER_FLAG1,
+                Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
+                Channels.COLUMN_INTERNAL_PROVIDER_FLAG3,
+                Channels.COLUMN_INTERNAL_PROVIDER_FLAG4,
+        };
+        String[] oReleaseColumns = new String[] {
+                Channels.COLUMN_TRANSIENT,
+        };
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return CollectionUtils.concatAll(baseColumns, marshmallowColumns);
+        }
+        if (BuildCompat.isAtLeastO()) {
+            return CollectionUtils.concatAll(baseColumns, marshmallowColumns, oReleaseColumns);
+        }
+        return baseColumns;
+    }
+
+    /**
+     * The builder class that makes it easy to chain setters to create a {@link Channel} object.
+     */
+    public static final class Builder {
+        private long mId = INVALID_CHANNEL_ID;
+        private String mPackageName;
+        private String mInputId;
+        private String mType;
+        private String mDisplayNumber;
+        private String mDisplayName;
+        private String mDescription;
+        private String mChannelLogo;
+        private String mVideoFormat;
+        private int mOriginalNetworkId = INVALID_INTEGER_VALUE;
+        private int mTransportStreamId;
+        private int mServiceId;
+        private String mAppLinkText;
+        private int mAppLinkColor;
+        private String mAppLinkIconUri;
+        private String mAppLinkPosterArtUri;
+        private String mAppLinkIntentUri;
+        private byte[] mInternalProviderData;
+        private String mNetworkAffiliation;
+        private int mSearchable;
+        private String mServiceType = Channels.SERVICE_TYPE_AUDIO_VIDEO;
+        private Long mInternalProviderFlag1;
+        private Long mInternalProviderFlag2;
+        private Long mInternalProviderFlag3;
+        private Long mInternalProviderFlag4;
+        private int mTransient;
+
+        public Builder() {
+        }
+
+        public Builder(Channel other) {
+            mId = other.mId;
+            mPackageName = other.mPackageName;
+            mInputId = other.mInputId;
+            mType = other.mType;
+            mDisplayNumber = other.mDisplayNumber;
+            mDisplayName = other.mDisplayName;
+            mDescription = other.mDescription;
+            mVideoFormat = other.mVideoFormat;
+            mOriginalNetworkId = other.mOriginalNetworkId;
+            mTransportStreamId = other.mTransportStreamId;
+            mServiceId = other.mServiceId;
+            mAppLinkText = other.mAppLinkText;
+            mAppLinkColor = other.mAppLinkColor;
+            mAppLinkIconUri = other.mAppLinkIconUri;
+            mAppLinkPosterArtUri = other.mAppLinkPosterArtUri;
+            mAppLinkIntentUri = other.mAppLinkIntentUri;
+            mChannelLogo = other.mChannelLogo;
+            mInternalProviderData = other.mInternalProviderData;
+            mNetworkAffiliation = other.mNetworkAffiliation;
+            mSearchable = other.mSearchable;
+            mServiceType = other.mServiceType;
+            mInternalProviderFlag1 = other.mInternalProviderFlag1;
+            mInternalProviderFlag2 = other.mInternalProviderFlag2;
+            mInternalProviderFlag3 = other.mInternalProviderFlag3;
+            mInternalProviderFlag4 = other.mInternalProviderFlag4;
+            mTransient = other.mTransient;
+        }
+
+        /**
+         * Sets the ID of the Channel.
+         *
+         * @param id The value of {@link Channels#_ID} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        private Builder setId(long id) {
+            mId = id;
+            return this;
+        }
+
+        /**
+         * Sets the package name of the Channel.
+         *
+         * @param packageName The value of {@link Channels#COLUMN_PACKAGE_NAME} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setPackageName(String packageName) {
+            mPackageName = packageName;
+            return this;
+        }
+
+        /**
+         * Sets the input id of the Channel.
+         *
+         * @param inputId The value of {@link Channels#COLUMN_INPUT_ID} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInputId(String inputId) {
+            mInputId = inputId;
+            return this;
+        }
+
+        /**
+         * Sets the broadcast standard of the Channel.
+         *
+         * @param type The value of {@link Channels#COLUMN_TYPE} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setType(String type) {
+            mType = type;
+            return this;
+        }
+
+        /**
+         * Sets the display number of the Channel.
+         *
+         * @param displayNumber The value of {@link Channels#COLUMN_DISPLAY_NUMBER} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setDisplayNumber(String displayNumber) {
+            mDisplayNumber = displayNumber;
+            return this;
+        }
+
+        /**
+         * Sets the name to be displayed for the Channel.
+         *
+         * @param displayName The value of {@link Channels#COLUMN_DISPLAY_NAME} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setDisplayName(String displayName) {
+            mDisplayName = displayName;
+            return this;
+        }
+
+        /**
+         * Sets the description of the Channel.
+         *
+         * @param description The value of {@link Channels#COLUMN_DESCRIPTION} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setDescription(String description) {
+            mDescription = description;
+            return this;
+        }
+
+        /**
+         * Sets the logo of the channel.
+         *
+         * @param channelLogo The Uri corresponding to the logo for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         * @see Channels.Logo
+         */
+        public Builder setChannelLogo(String channelLogo) {
+            mChannelLogo = channelLogo;
+            return this;
+        }
+
+        /**
+         * Sets the video format of the Channel.
+         *
+         * @param videoFormat The value of {@link Channels#COLUMN_VIDEO_FORMAT} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setVideoFormat(String videoFormat) {
+            mVideoFormat = videoFormat;
+            return this;
+        }
+
+        /**
+         * Sets the original network id of the Channel.
+         *
+         * @param originalNetworkId The value of {@link Channels#COLUMN_ORIGINAL_NETWORK_ID} for the
+         *                          channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setOriginalNetworkId(int originalNetworkId) {
+            mOriginalNetworkId = originalNetworkId;
+            return this;
+        }
+
+        /**
+         * Sets the transport stream id of the Channel.
+         *
+         * @param transportStreamId The value of {@link Channels#COLUMN_TRANSPORT_STREAM_ID} for the
+         *                          channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setTransportStreamId(int transportStreamId) {
+            mTransportStreamId = transportStreamId;
+            return this;
+        }
+
+        /**
+         * Sets the service id of the Channel.
+         *
+         * @param serviceId The value of {@link Channels#COLUMN_SERVICE_ID} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setServiceId(int serviceId) {
+            mServiceId = serviceId;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider data of the channel.
+         *
+         * @param internalProviderData The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_DATA}
+         *                             for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderData(byte[] internalProviderData) {
+            mInternalProviderData = internalProviderData;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider data of the channel.
+         *
+         * @param internalProviderData The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_DATA}
+         *                             for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderData(String internalProviderData) {
+            mInternalProviderData = internalProviderData.getBytes();
+            return this;
+        }
+
+        /**
+         * Sets the text to be displayed in the App Linking card.
+         *
+         * @param appLinkText The value of {@link Channels#COLUMN_APP_LINK_TEXT} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setAppLinkText(String appLinkText) {
+            mAppLinkText = appLinkText;
+            return this;
+        }
+
+        /**
+         * Sets the background color of the App Linking card.
+         *
+         * @param appLinkColor The value of {@link Channels#COLUMN_APP_LINK_COLOR} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setAppLinkColor(int appLinkColor) {
+            mAppLinkColor = appLinkColor;
+            return this;
+        }
+
+        /**
+         * Sets the icon to be displayed next to the text of the App Linking card.
+         *
+         * @param appLinkIconUri The value of {@link Channels#COLUMN_APP_LINK_ICON_URI} for the
+         *                       channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setAppLinkIconUri(String appLinkIconUri) {
+            mAppLinkIconUri = appLinkIconUri;
+            return this;
+        }
+
+        /**
+         * Sets the background image of the App Linking card.
+         *
+         * @param appLinkPosterArtUri The value of {@link Channels#COLUMN_APP_LINK_POSTER_ART_URI}
+         *                            for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setAppLinkPosterArtUri(String appLinkPosterArtUri) {
+            mAppLinkPosterArtUri = appLinkPosterArtUri;
+            return this;
+        }
+
+        /**
+         * Sets the App Linking Intent.
+         *
+         * @param appLinkIntent The Intent to be executed when the App Linking card is selected
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setAppLinkIntent(Intent appLinkIntent) {
+            return setAppLinkIntentUri(appLinkIntent.toUri(Intent.URI_INTENT_SCHEME));
+        }
+
+        /**
+         * Sets the App Linking Intent.
+         *
+         * @param appLinkIntentUri The Intent that should be executed when the App Linking card is
+         *                         selected. Use the method toUri(Intent.URI_INTENT_SCHEME) on your
+         *                         Intent to turn it into a String. See
+         *                         {@link Channels#COLUMN_APP_LINK_INTENT_URI}.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setAppLinkIntentUri(String appLinkIntentUri) {
+            mAppLinkIntentUri = appLinkIntentUri;
+            return this;
+        }
+
+        /**
+         * Sets the network name for the channel, which may be different from its display name.
+         *
+         * @param networkAffiliation The value of
+         * {@link Channels#COLUMN_NETWORK_AFFILIATION} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setNetworkAffiliation(String networkAffiliation) {
+            mNetworkAffiliation = networkAffiliation;
+            return this;
+        }
+
+        /**
+         * Sets whether this channel can be searched for in other applications.
+         *
+         * @param searchable The value of {@link Channels#COLUMN_SEARCHABLE} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setSearchable(boolean searchable) {
+            mSearchable = searchable ? IS_SEARCHABLE : 0;
+            return this;
+        }
+
+        /**
+         * Sets the type of content that will appear on this channel. This could refer to the
+         * underlying broadcast standard or refer to {@link Channels#SERVICE_TYPE_AUDIO},
+         * {@link Channels#SERVICE_TYPE_AUDIO_VIDEO}, or {@link Channels#SERVICE_TYPE_OTHER}.
+         *
+         * @param serviceType The value of {@link Channels#COLUMN_SERVICE_TYPE} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setServiceType(String serviceType) {
+            mServiceType = serviceType;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider flag1 for the channel.
+         *
+         * @param flag The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG1} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderFlag1(long flag) {
+            mInternalProviderFlag1 = flag;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider flag2 for the channel.
+         *
+         * @param flag The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG2} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderFlag2(long flag) {
+            mInternalProviderFlag2 = flag;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider flag3 for the channel.
+         *
+         * @param flag The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG3} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderFlag3(long flag) {
+            mInternalProviderFlag3 = flag;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider flag4 for the channel.
+         *
+         * @param flag The value of {@link Channels#COLUMN_INTERNAL_PROVIDER_FLAG4} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderFlag4(long flag) {
+            mInternalProviderFlag4 = flag;
+            return this;
+        }
+
+        /**
+         * Sets whether this channel is transient or not.
+         *
+         * @param value The value of {@link Channels#COLUMN_TRANSIENT} for the channel.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public Builder setTransient(boolean value) {
+            mTransient = value ? IS_TRANSIENT : 0;
+            return this;
+        }
+
+        /**
+         * Takes the values of the Builder object and creates a Channel object.
+         * @return Channel object with values from the Builder.
+         */
+        public Channel build() {
+            return new Channel(this);
+        }
+    }
+}
diff --git a/tv-provider/src/android/support/media/tv/CollectionUtils.java b/tv-provider/src/android/support/media/tv/CollectionUtils.java
new file mode 100644
index 0000000..7aa1074
--- /dev/null
+++ b/tv-provider/src/android/support/media/tv/CollectionUtils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.tv;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.support.annotation.RestrictTo;
+
+import java.util.Arrays;
+
+/**
+ * Static utilities for collections
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class CollectionUtils {
+    /**
+     * Returns an array with the arrays concatenated together.
+     *
+     * @see <a href="http://stackoverflow.com/a/784842/1122089">Stackoverflow answer</a> by
+     *      <a href="http://stackoverflow.com/users/40342/joachim-sauer">Joachim Sauer</a>
+     */
+    public static <T> T[] concatAll(T[] first, T[]... rest) {
+        int totalLength = first.length;
+        for (T[] array : rest) {
+            totalLength += array.length;
+        }
+        T[] result = Arrays.copyOf(first, totalLength);
+        int offset = first.length;
+        for (T[] array : rest) {
+            System.arraycopy(array, 0, result, offset, array.length);
+            offset += array.length;
+        }
+        return result;
+    }
+}
diff --git a/tv-provider/src/android/support/media/tv/Program.java b/tv-provider/src/android/support/media/tv/Program.java
new file mode 100644
index 0000000..e7beed5
--- /dev/null
+++ b/tv-provider/src/android/support/media/tv/Program.java
@@ -0,0 +1,1332 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.tv;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.media.tv.TvContentRating;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+import android.support.media.tv.TvContractCompat.Programs;
+import android.support.v4.os.BuildCompat;
+import android.text.TextUtils;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A convenience class to create and insert program information into the database.
+ */
+public final class Program implements Comparable<Program> {
+    /**
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String[] PROJECTION = getProjection();
+
+    private static final long INVALID_LONG_VALUE = -1;
+    private static final int INVALID_INT_VALUE = -1;
+    private static final int IS_RECORDING_PROHIBITED = 1;
+    private static final int IS_SEARCHABLE = 1;
+    private static final int IS_TRANSIENT = 1;
+
+    private final long mId;
+    private final long mChannelId;
+    private final String mTitle;
+    private final String mEpisodeTitle;
+    private final String mSeasonNumber;
+    private final String mEpisodeNumber;
+    private final long mStartTimeUtcMillis;
+    private final long mEndTimeUtcMillis;
+    private final String mDescription;
+    private final String mLongDescription;
+    private final int mVideoWidth;
+    private final int mVideoHeight;
+    private final String mPosterArtUri;
+    private final String mThumbnailUri;
+    private final String[] mBroadcastGenres;
+    private final String[] mCanonicalGenres;
+    private final TvContentRating[] mContentRatings;
+    private final byte[] mInternalProviderData;
+    private final String[] mAudioLanguages;
+    private final int mSearchable;
+    private final Long mInternalProviderFlag1;
+    private final Long mInternalProviderFlag2;
+    private final Long mInternalProviderFlag3;
+    private final Long mInternalProviderFlag4;
+    private final int mRecordingProhibited;
+    private final String mSeasonTitle;
+    private final String mInternalProviderId;
+    private final String mPreviewVideoUri;
+    private final int mPreviewLastPlaybackPosition;
+    private final int mPreviewDuration;
+    private final String mPreviewAppLinkIntentUri;
+    private final int mPreviewWeight;
+    private final int mTransient;
+
+    private Program(Builder builder) {
+        mId = builder.mId;
+        mChannelId = builder.mChannelId;
+        mTitle = builder.mTitle;
+        mEpisodeTitle = builder.mEpisodeTitle;
+        mSeasonNumber = builder.mSeasonNumber;
+        mEpisodeNumber = builder.mEpisodeNumber;
+        mStartTimeUtcMillis = builder.mStartTimeUtcMillis;
+        mEndTimeUtcMillis = builder.mEndTimeUtcMillis;
+        mDescription = builder.mDescription;
+        mLongDescription = builder.mLongDescription;
+        mVideoWidth = builder.mVideoWidth;
+        mVideoHeight = builder.mVideoHeight;
+        mPosterArtUri = builder.mPosterArtUri;
+        mThumbnailUri = builder.mThumbnailUri;
+        mBroadcastGenres = builder.mBroadcastGenres;
+        mCanonicalGenres = builder.mCanonicalGenres;
+        mContentRatings = builder.mContentRatings;
+        mInternalProviderData = builder.mInternalProviderData;
+        mAudioLanguages = builder.mAudioLanguages;
+        mSearchable = builder.mSearchable;
+        mInternalProviderFlag1 = builder.mInternalProviderFlag1;
+        mInternalProviderFlag2 = builder.mInternalProviderFlag2;
+        mInternalProviderFlag3 = builder.mInternalProviderFlag3;
+        mInternalProviderFlag4 = builder.mInternalProviderFlag4;
+        mRecordingProhibited = builder.mRecordingProhibited;
+        mSeasonTitle = builder.mSeasonTitle;
+        mInternalProviderId = builder.mExternalId;
+        mPreviewVideoUri = builder.mPreviewVideoUri;
+        mPreviewLastPlaybackPosition = builder.mPreviewPosition;
+        mPreviewDuration = builder.mPreviewDuration;
+        mPreviewAppLinkIntentUri = builder.mPreviewAppLinkIntentUri;
+        mPreviewWeight = builder.mWeight;
+        mTransient = builder.mTransient;
+    }
+
+    /**
+     * @return The value of {@link Programs#_ID} for the program.
+     */
+    public long getId() {
+        return mId;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_CHANNEL_ID} for the program.
+     */
+    public long getChannelId() {
+        return mChannelId;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_TITLE} for the program.
+     */
+    public String getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_EPISODE_TITLE} for the program.
+     */
+    public String getEpisodeTitle() {
+        return mEpisodeTitle;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_SEASON_DISPLAY_NUMBER} for the program.
+     */
+    public String getSeasonNumber() {
+        return mSeasonNumber;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_EPISODE_DISPLAY_NUMBER} for the program.
+     */
+    public String getEpisodeNumber() {
+        return mEpisodeNumber;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_START_TIME_UTC_MILLIS} for the program.
+     */
+    public long getStartTimeUtcMillis() {
+        return mStartTimeUtcMillis;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_END_TIME_UTC_MILLIS} for the program.
+     */
+    public long getEndTimeUtcMillis() {
+        return mEndTimeUtcMillis;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_SHORT_DESCRIPTION} for the program.
+     */
+    public String getDescription() {
+        return mDescription;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_LONG_DESCRIPTION} for the program.
+     */
+    public String getLongDescription() {
+        return mLongDescription;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_VIDEO_WIDTH} for the program.
+     */
+    public int getVideoWidth() {
+        return mVideoWidth;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_VIDEO_HEIGHT} for the program.
+     */
+    public int getVideoHeight() {
+        return mVideoHeight;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_BROADCAST_GENRE} for the program.
+     */
+    public String[] getBroadcastGenres() {
+        return mBroadcastGenres;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_CANONICAL_GENRE} for the program.
+     */
+    public String[] getCanonicalGenres() {
+        return mCanonicalGenres;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_CONTENT_RATING} for the program.
+     */
+    public TvContentRating[] getContentRatings() {
+        return mContentRatings;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_POSTER_ART_URI} for the program.
+     */
+    public String getPosterArtUri() {
+        return mPosterArtUri;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_THUMBNAIL_URI} for the program.
+     */
+    public String getThumbnailUri() {
+        return mThumbnailUri;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_DATA} for the program.
+     */
+    public byte[] getInternalProviderDataByteArray() {
+        return mInternalProviderData;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_AUDIO_LANGUAGE} for the program.
+     */
+    public String[] getAudioLanguages() {
+        return mAudioLanguages;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_SEARCHABLE} for the program.
+     */
+    public boolean isSearchable() {
+        return mSearchable == IS_SEARCHABLE || mSearchable == INVALID_INT_VALUE;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_FLAG1} for the program.
+     */
+    public Long getInternalProviderFlag1() {
+        return mInternalProviderFlag1;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_FLAG2} for the program.
+     */
+    public Long getInternalProviderFlag2() {
+        return mInternalProviderFlag2;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_FLAG3} for the program.
+     */
+    public Long getInternalProviderFlag3() {
+        return mInternalProviderFlag3;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_FLAG4} for the program.
+     */
+    public Long getInternalProviderFlag4() {
+        return mInternalProviderFlag4;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_RECORDING_PROHIBITED} for the program.
+     */
+    public boolean isRecordingProhibited() {
+        return mRecordingProhibited == IS_RECORDING_PROHIBITED;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_SEASON_TITLE} for the program.
+     */
+    public String getSeasonTitle() {
+        return mSeasonTitle;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_ID} for the program.
+     */
+    public String getInternalProviderId() {
+        return mInternalProviderId;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_PREVIEW_VIDEO_URI} for the program.
+     */
+    public String getPreviewVideoUri() {
+        return mPreviewVideoUri;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_PREVIEW_LAST_PLAYBACK_POSITION} for the program.
+     */
+    public int getPreviewLastPlaybackPosition() {
+        return mPreviewLastPlaybackPosition;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_PREVIEW_DURATION} for the program.
+     */
+    public int getPreviewDuration() {
+        return mPreviewDuration;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_PREVIEW_INTENT_URI} for the program.
+     */
+    public String getPreviewAppLinkIntentUri() {
+        return mPreviewAppLinkIntentUri;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_PREVIEW_WEIGHT} for the program.
+     */
+    public int getPreviewWeight() {
+        return mPreviewWeight;
+    }
+
+    /**
+     * @return The value of {@link Programs#COLUMN_TRANSIENT} for the program.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public boolean isTransient() {
+        return mTransient == IS_TRANSIENT;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mChannelId, mStartTimeUtcMillis, mEndTimeUtcMillis,
+                mTitle, mEpisodeTitle, mDescription, mLongDescription, mVideoWidth, mVideoHeight,
+                mPosterArtUri, mThumbnailUri, mContentRatings, mCanonicalGenres, mSeasonNumber,
+                mEpisodeNumber);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (!(other instanceof Program)) {
+            return false;
+        }
+        Program program = (Program) other;
+        return mChannelId == program.mChannelId
+                && mStartTimeUtcMillis == program.mStartTimeUtcMillis
+                && mEndTimeUtcMillis == program.mEndTimeUtcMillis
+                && Objects.equals(mTitle, program.mTitle)
+                && Objects.equals(mEpisodeTitle, program.mEpisodeTitle)
+                && Objects.equals(mSeasonNumber, program.mSeasonNumber)
+                && Objects.equals(mEpisodeNumber, program.mEpisodeNumber)
+                && Objects.equals(mDescription, program.mDescription)
+                && Objects.equals(mLongDescription, program.mLongDescription)
+                && mVideoWidth == program.mVideoWidth
+                && mVideoHeight == program.mVideoHeight
+                && Objects.equals(mPosterArtUri, program.mPosterArtUri)
+                && Objects.equals(mThumbnailUri, program.mThumbnailUri)
+                && Arrays.equals(mInternalProviderData, program.mInternalProviderData)
+                && Arrays.equals(mBroadcastGenres, program.mBroadcastGenres)
+                && Arrays.equals(mCanonicalGenres, program.mCanonicalGenres)
+                && Arrays.equals(mContentRatings, program.mContentRatings)
+                && Arrays.equals(mAudioLanguages, program.mAudioLanguages)
+                && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
+                        || Objects.equals(mSearchable, program.mSearchable)
+                        && Objects.equals(mInternalProviderFlag1, program.mInternalProviderFlag1)
+                        && Objects.equals(mInternalProviderFlag2, program.mInternalProviderFlag2)
+                        && Objects.equals(mInternalProviderFlag3, program.mInternalProviderFlag3)
+                        && Objects.equals(mInternalProviderFlag4, program.mInternalProviderFlag4))
+                && (Build.VERSION.SDK_INT < Build.VERSION_CODES.N
+                        || Objects.equals(mSeasonTitle, program.mSeasonTitle)
+                        && Objects.equals(mRecordingProhibited, program.mRecordingProhibited))
+                && (!BuildCompat.isAtLeastO()
+                        || Objects.equals(mInternalProviderId, program.mInternalProviderId)
+                        && Objects.equals(mPreviewVideoUri, program.mPreviewVideoUri)
+                        && Objects.equals(mPreviewLastPlaybackPosition,
+                                program.mPreviewLastPlaybackPosition)
+                        && Objects.equals(mPreviewDuration, program.mPreviewDuration)
+                        && Objects.equals(mPreviewAppLinkIntentUri,
+                                program.mPreviewAppLinkIntentUri)
+                        && Objects.equals(mPreviewWeight, program.mPreviewWeight)
+                        && Objects.equals(mTransient, program.mTransient));
+    }
+
+    /**
+     * @param other The program you're comparing to.
+     * @return The chronological order of the programs.
+     */
+    @Override
+    public int compareTo(@NonNull Program other) {
+        return Long.compare(mStartTimeUtcMillis, other.mStartTimeUtcMillis);
+    }
+
+    @Override
+    public String toString() {
+        return "Program{"
+                + "id=" + mId
+                + ", channelId=" + mChannelId
+                + ", title=" + mTitle
+                + ", episodeTitle=" + mEpisodeTitle
+                + ", seasonNumber=" + mSeasonNumber
+                + ", episodeNumber=" + mEpisodeNumber
+                + ", startTimeUtcSec=" + mStartTimeUtcMillis
+                + ", endTimeUtcSec=" + mEndTimeUtcMillis
+                + ", videoWidth=" + mVideoWidth
+                + ", videoHeight=" + mVideoHeight
+                + ", contentRatings=" + Arrays.toString(mContentRatings)
+                + ", posterArtUri=" + mPosterArtUri
+                + ", thumbnailUri=" + mThumbnailUri
+                + ", contentRatings=" + Arrays.toString(mContentRatings)
+                + ", genres=" + Arrays.toString(mCanonicalGenres)
+                + "}";
+    }
+
+    /**
+     * @return The fields of the Program in the ContentValues format to be easily inserted into the
+     * TV Input Framework database.
+     */
+    public ContentValues toContentValues() {
+        ContentValues values = new ContentValues();
+        if (mId != INVALID_LONG_VALUE) {
+            values.put(Programs._ID, mId);
+        }
+        if (mChannelId != INVALID_LONG_VALUE) {
+            values.put(Programs.COLUMN_CHANNEL_ID, mChannelId);
+        } else {
+            values.putNull(Programs.COLUMN_CHANNEL_ID);
+        }
+        if (!TextUtils.isEmpty(mTitle)) {
+            values.put(Programs.COLUMN_TITLE, mTitle);
+        } else {
+            values.putNull(Programs.COLUMN_TITLE);
+        }
+        if (!TextUtils.isEmpty(mEpisodeTitle)) {
+            values.put(Programs.COLUMN_EPISODE_TITLE, mEpisodeTitle);
+        } else {
+            values.putNull(Programs.COLUMN_EPISODE_TITLE);
+        }
+        if (!TextUtils.isEmpty(mSeasonNumber) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            values.put(Programs.COLUMN_SEASON_DISPLAY_NUMBER, mSeasonNumber);
+        } else if (!TextUtils.isEmpty(mSeasonNumber)
+                && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+            values.put(Programs.COLUMN_SEASON_NUMBER,
+                    Integer.parseInt(mSeasonNumber));
+        } else {
+            values.putNull(Programs.COLUMN_SEASON_NUMBER);
+        }
+        if (!TextUtils.isEmpty(mEpisodeNumber) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            values.put(Programs.COLUMN_EPISODE_DISPLAY_NUMBER, mEpisodeNumber);
+        } else if (!TextUtils.isEmpty(mEpisodeNumber)
+                && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+            values.put(Programs.COLUMN_EPISODE_NUMBER,
+                    Integer.parseInt(mEpisodeNumber));
+        } else {
+            values.putNull(Programs.COLUMN_EPISODE_NUMBER);
+        }
+        if (!TextUtils.isEmpty(mDescription)) {
+            values.put(Programs.COLUMN_SHORT_DESCRIPTION, mDescription);
+        } else {
+            values.putNull(Programs.COLUMN_SHORT_DESCRIPTION);
+        }
+        if (!TextUtils.isEmpty(mDescription)) {
+            values.put(Programs.COLUMN_LONG_DESCRIPTION, mLongDescription);
+        } else {
+            values.putNull(Programs.COLUMN_LONG_DESCRIPTION);
+        }
+        if (!TextUtils.isEmpty(mPosterArtUri)) {
+            values.put(Programs.COLUMN_POSTER_ART_URI, mPosterArtUri);
+        } else {
+            values.putNull(Programs.COLUMN_POSTER_ART_URI);
+        }
+        if (!TextUtils.isEmpty(mThumbnailUri)) {
+            values.put(Programs.COLUMN_THUMBNAIL_URI, mThumbnailUri);
+        } else {
+            values.putNull(Programs.COLUMN_THUMBNAIL_URI);
+        }
+        if (mAudioLanguages != null && mAudioLanguages.length > 0) {
+            values.put(Programs.COLUMN_AUDIO_LANGUAGE,
+                    TvContractUtils.audioLanguagesToString(mAudioLanguages));
+        } else {
+            values.putNull(Programs.COLUMN_AUDIO_LANGUAGE);
+        }
+        if (mBroadcastGenres != null && mBroadcastGenres.length > 0) {
+            values.put(Programs.COLUMN_BROADCAST_GENRE,
+                    Programs.Genres.encode(mBroadcastGenres));
+        } else {
+            values.putNull(Programs.COLUMN_BROADCAST_GENRE);
+        }
+        if (mCanonicalGenres != null && mCanonicalGenres.length > 0) {
+            values.put(Programs.COLUMN_CANONICAL_GENRE,
+                    Programs.Genres.encode(mCanonicalGenres));
+        } else {
+            values.putNull(Programs.COLUMN_CANONICAL_GENRE);
+        }
+        if (mContentRatings != null && mContentRatings.length > 0) {
+            values.put(Programs.COLUMN_CONTENT_RATING,
+                    TvContractUtils.contentRatingsToString(mContentRatings));
+        } else {
+            values.putNull(Programs.COLUMN_CONTENT_RATING);
+        }
+        if (mStartTimeUtcMillis != INVALID_LONG_VALUE) {
+            values.put(Programs.COLUMN_START_TIME_UTC_MILLIS, mStartTimeUtcMillis);
+        } else {
+            values.putNull(Programs.COLUMN_START_TIME_UTC_MILLIS);
+        }
+        if (mEndTimeUtcMillis != INVALID_LONG_VALUE) {
+            values.put(Programs.COLUMN_END_TIME_UTC_MILLIS, mEndTimeUtcMillis);
+        } else {
+            values.putNull(Programs.COLUMN_END_TIME_UTC_MILLIS);
+        }
+        if (mVideoWidth != INVALID_INT_VALUE) {
+            values.put(Programs.COLUMN_VIDEO_WIDTH, mVideoWidth);
+        } else {
+            values.putNull(Programs.COLUMN_VIDEO_WIDTH);
+        }
+        if (mVideoHeight != INVALID_INT_VALUE) {
+            values.put(Programs.COLUMN_VIDEO_HEIGHT, mVideoHeight);
+        } else {
+            values.putNull(Programs.COLUMN_VIDEO_HEIGHT);
+        }
+        if (mInternalProviderData != null && mInternalProviderData.length > 0) {
+            values.put(Programs.COLUMN_INTERNAL_PROVIDER_DATA,
+                    mInternalProviderData);
+        } else {
+            values.putNull(Programs.COLUMN_INTERNAL_PROVIDER_DATA);
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            if (mSearchable != INVALID_INT_VALUE) {
+                values.put(Programs.COLUMN_SEARCHABLE, mSearchable);
+            }
+            if (mInternalProviderFlag1 != null) {
+                values.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG1, mInternalProviderFlag1);
+            }
+            if (mInternalProviderFlag2 != null) {
+                values.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG2, mInternalProviderFlag2);
+            }
+            if (mInternalProviderFlag3 != null) {
+                values.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG3, mInternalProviderFlag3);
+            }
+            if (mInternalProviderFlag4 != null) {
+                values.put(Programs.COLUMN_INTERNAL_PROVIDER_FLAG4, mInternalProviderFlag4);
+            }
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            if (!TextUtils.isEmpty(mSeasonTitle)) {
+                values.put(Programs.COLUMN_SEASON_TITLE, mSeasonTitle);
+            }
+            if (mRecordingProhibited != INVALID_INT_VALUE) {
+                values.put(Programs.COLUMN_RECORDING_PROHIBITED, mRecordingProhibited);
+            }
+        }
+        if (BuildCompat.isAtLeastO()) {
+            if (!TextUtils.isEmpty(mInternalProviderId)) {
+                values.put(Programs.COLUMN_INTERNAL_PROVIDER_ID, mInternalProviderId);
+            }
+            if (!TextUtils.isEmpty(mPreviewVideoUri)) {
+                values.put(Programs.COLUMN_PREVIEW_VIDEO_URI, mPreviewVideoUri);
+            }
+            if (mPreviewLastPlaybackPosition != INVALID_INT_VALUE) {
+                values.put(Programs.COLUMN_PREVIEW_LAST_PLAYBACK_POSITION,
+                        mPreviewLastPlaybackPosition);
+            }
+            if (mPreviewDuration != INVALID_INT_VALUE) {
+                values.put(Programs.COLUMN_PREVIEW_DURATION, mPreviewDuration);
+            }
+            if (!TextUtils.isEmpty(mPreviewAppLinkIntentUri)) {
+                values.put(Programs.COLUMN_PREVIEW_INTENT_URI, mPreviewAppLinkIntentUri);
+            }
+            if (mPreviewWeight != INVALID_INT_VALUE) {
+                values.put(Programs.COLUMN_PREVIEW_WEIGHT, mPreviewWeight);
+            }
+            if (mTransient == IS_TRANSIENT) {
+                values.put(Programs.COLUMN_TRANSIENT, mTransient);
+            }
+        }
+        return values;
+    }
+
+    /**
+     * Creates a Program object from a cursor including the fields defined in {@link Programs}.
+     *
+     * @param cursor A row from the TV Input Framework database.
+     * @return A Program with the values taken from the cursor.
+     */
+    public static Program fromCursor(Cursor cursor) {
+        // TODO: Add additional API which does not use costly getColumnIndex().
+        Builder builder = new Builder();
+        int index;
+        if ((index = cursor.getColumnIndex(Programs._ID)) >= 0 && !cursor.isNull(index)) {
+            builder.setId(cursor.getLong(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_CHANNEL_ID)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setChannelId(cursor.getLong(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_TITLE)) >= 0 && !cursor.isNull(index)) {
+            builder.setTitle(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_EPISODE_TITLE)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setEpisodeTitle(cursor.getString(index));
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_SEASON_DISPLAY_NUMBER)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setSeasonNumber(cursor.getString(index), INVALID_INT_VALUE);
+            }
+        } else {
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_SEASON_NUMBER)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setSeasonNumber(cursor.getInt(index));
+            }
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_EPISODE_DISPLAY_NUMBER)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setEpisodeNumber(cursor.getString(index), INVALID_INT_VALUE);
+            }
+        } else {
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_EPISODE_NUMBER)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setEpisodeNumber(cursor.getInt(index));
+            }
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_SHORT_DESCRIPTION)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setDescription(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_LONG_DESCRIPTION)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setLongDescription(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_POSTER_ART_URI)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setPosterArtUri(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_THUMBNAIL_URI)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setThumbnailUri(cursor.getString(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_AUDIO_LANGUAGE)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setAudioLanguages(
+                    TvContractUtils.stringToAudioLanguages(cursor.getString(index)));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_BROADCAST_GENRE)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setBroadcastGenres(Programs.Genres.decode(
+                    cursor.getString(index)));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_CANONICAL_GENRE)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setCanonicalGenres(Programs.Genres.decode(
+                    cursor.getString(index)));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_CONTENT_RATING)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setContentRatings(
+                    TvContractUtils.stringToContentRatings(cursor.getString(index)));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_START_TIME_UTC_MILLIS)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setStartTimeUtcMillis(cursor.getLong(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_END_TIME_UTC_MILLIS)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setEndTimeUtcMillis(cursor.getLong(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_VIDEO_WIDTH)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setVideoWidth((int) cursor.getLong(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_VIDEO_HEIGHT)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setVideoHeight((int) cursor.getLong(index));
+        }
+        if ((index = cursor.getColumnIndex(Programs.COLUMN_INTERNAL_PROVIDER_DATA)) >= 0
+                && !cursor.isNull(index)) {
+            builder.setInternalProviderData(cursor.getBlob(index));
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_SEARCHABLE)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setSearchable(cursor.getInt(index) == IS_SEARCHABLE);
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_INTERNAL_PROVIDER_FLAG1)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setInternalProviderFlag1(cursor.getLong(index));
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_INTERNAL_PROVIDER_FLAG2)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setInternalProviderFlag2(cursor.getLong(index));
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_INTERNAL_PROVIDER_FLAG3)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setInternalProviderFlag3(cursor.getLong(index));
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_INTERNAL_PROVIDER_FLAG4)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setInternalProviderFlag4(cursor.getLong(index));
+            }
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_SEASON_TITLE)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setSeasonTitle(cursor.getString(index));
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_RECORDING_PROHIBITED)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setRecordingProhibited(cursor.getInt(index) == IS_RECORDING_PROHIBITED);
+            }
+        }
+        if (BuildCompat.isAtLeastO()) {
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_INTERNAL_PROVIDER_ID)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setInternalProviderId(cursor.getString(index));
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_PREVIEW_VIDEO_URI)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setPreviewVideoUri(cursor.getString(index));
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_PREVIEW_LAST_PLAYBACK_POSITION)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setPreviewLastPlaybackPosition(cursor.getInt(index));
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_PREVIEW_DURATION)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setPreviewDuration(cursor.getInt(index));
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_PREVIEW_INTENT_URI)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setPreviewIntentUri(cursor.getString(index));
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_PREVIEW_WEIGHT)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setPreviewWeight(cursor.getInt(index));
+            }
+            if ((index = cursor.getColumnIndex(Programs.COLUMN_TRANSIENT)) >= 0
+                    && !cursor.isNull(index)) {
+                builder.setTransient(cursor.getInt(index) == IS_TRANSIENT);
+            }
+        }
+        return builder.build();
+    }
+
+    private static String[] getProjection() {
+        String[] baseColumns = new String[] {
+                Programs._ID,
+                Programs.COLUMN_CHANNEL_ID,
+                Programs.COLUMN_TITLE,
+                Programs.COLUMN_EPISODE_TITLE,
+                (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+                        ? Programs.COLUMN_SEASON_DISPLAY_NUMBER : Programs.COLUMN_SEASON_NUMBER,
+                (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+                        ? Programs.COLUMN_EPISODE_DISPLAY_NUMBER : Programs.COLUMN_EPISODE_NUMBER,
+                Programs.COLUMN_SHORT_DESCRIPTION,
+                Programs.COLUMN_LONG_DESCRIPTION,
+                Programs.COLUMN_POSTER_ART_URI,
+                Programs.COLUMN_THUMBNAIL_URI,
+                Programs.COLUMN_AUDIO_LANGUAGE,
+                Programs.COLUMN_BROADCAST_GENRE,
+                Programs.COLUMN_CANONICAL_GENRE,
+                Programs.COLUMN_CONTENT_RATING,
+                Programs.COLUMN_START_TIME_UTC_MILLIS,
+                Programs.COLUMN_END_TIME_UTC_MILLIS,
+                Programs.COLUMN_VIDEO_WIDTH,
+                Programs.COLUMN_VIDEO_HEIGHT,
+                Programs.COLUMN_INTERNAL_PROVIDER_DATA
+        };
+        String[] marshmallowColumns = new String[] {
+                Programs.COLUMN_SEARCHABLE,
+                Programs.COLUMN_INTERNAL_PROVIDER_FLAG1,
+                Programs.COLUMN_INTERNAL_PROVIDER_FLAG2,
+                Programs.COLUMN_INTERNAL_PROVIDER_FLAG3,
+                Programs.COLUMN_INTERNAL_PROVIDER_FLAG4,
+        };
+        String[] nougatColumns = new String[] {
+                Programs.COLUMN_SEASON_TITLE,
+                Programs.COLUMN_RECORDING_PROHIBITED
+        };
+        String[] oColumns = new String[] {
+                Programs.COLUMN_INTERNAL_PROVIDER_ID,
+                Programs.COLUMN_PREVIEW_VIDEO_URI,
+                Programs.COLUMN_PREVIEW_LAST_PLAYBACK_POSITION,
+                Programs.COLUMN_PREVIEW_DURATION,
+                Programs.COLUMN_PREVIEW_INTENT_URI,
+                Programs.COLUMN_PREVIEW_WEIGHT,
+                Programs.COLUMN_TRANSIENT,
+        };
+        if (BuildCompat.isAtLeastO()) {
+            return CollectionUtils.concatAll(baseColumns, marshmallowColumns, nougatColumns,
+                    oColumns);
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            return CollectionUtils.concatAll(baseColumns, marshmallowColumns, nougatColumns);
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return CollectionUtils.concatAll(baseColumns, marshmallowColumns);
+        } else {
+            return baseColumns;
+        }
+    }
+
+    /**
+     * This Builder class simplifies the creation of a {@link Program} object.
+     */
+    public static final class Builder {
+        private long mId = INVALID_LONG_VALUE;
+        private long mChannelId = INVALID_LONG_VALUE;
+        private String mTitle;
+        private String mEpisodeTitle;
+        private String mSeasonNumber;
+        private String mEpisodeNumber;
+        private long mStartTimeUtcMillis = INVALID_LONG_VALUE;
+        private long mEndTimeUtcMillis = INVALID_LONG_VALUE;
+        private String mDescription;
+        private String mLongDescription;
+        private int mVideoWidth = INVALID_INT_VALUE;
+        private int mVideoHeight = INVALID_INT_VALUE;
+        private String mPosterArtUri;
+        private String mThumbnailUri;
+        private String[] mBroadcastGenres;
+        private String[] mCanonicalGenres;
+        private TvContentRating[] mContentRatings;
+        private byte[] mInternalProviderData;
+        private String[] mAudioLanguages;
+        private int mSearchable = INVALID_INT_VALUE;
+        private Long mInternalProviderFlag1;
+        private Long mInternalProviderFlag2;
+        private Long mInternalProviderFlag3;
+        private Long mInternalProviderFlag4;
+        private int mRecordingProhibited = INVALID_INT_VALUE;
+        private String mSeasonTitle;
+        private String mExternalId;
+        private String mPreviewVideoUri;
+        private int mPreviewPosition = INVALID_INT_VALUE;
+        private int mPreviewDuration = INVALID_INT_VALUE;
+        private String mPreviewAppLinkIntentUri;
+        private int mWeight = INVALID_INT_VALUE;
+        private int mTransient;
+
+        /**
+         * Creates a new Builder object.
+         */
+        public Builder() {
+        }
+
+        /**
+         * Creates a new Builder object with values copied from another Program.
+         * @param other The Program you're copying from.
+         */
+        public Builder(Program other) {
+            mId = other.mId;
+            mChannelId = other.mChannelId;
+            mTitle = other.mTitle;
+            mEpisodeTitle = other.mEpisodeTitle;
+            mSeasonNumber = other.mSeasonNumber;
+            mEpisodeNumber = other.mEpisodeNumber;
+            mStartTimeUtcMillis = other.mStartTimeUtcMillis;
+            mEndTimeUtcMillis = other.mEndTimeUtcMillis;
+            mDescription = other.mDescription;
+            mLongDescription = other.mLongDescription;
+            mVideoWidth = other.mVideoWidth;
+            mVideoHeight = other.mVideoHeight;
+            mPosterArtUri = other.mPosterArtUri;
+            mThumbnailUri = other.mThumbnailUri;
+            mBroadcastGenres = other.mBroadcastGenres;
+            mCanonicalGenres = other.mCanonicalGenres;
+            mContentRatings = other.mContentRatings;
+            mInternalProviderData = other.mInternalProviderData;
+            mAudioLanguages = other.mAudioLanguages;
+            mSearchable = other.mSearchable;
+            mInternalProviderFlag1 = other.mInternalProviderFlag1;
+            mInternalProviderFlag2 = other.mInternalProviderFlag2;
+            mInternalProviderFlag3 = other.mInternalProviderFlag3;
+            mInternalProviderFlag4 = other.mInternalProviderFlag4;
+            mRecordingProhibited = other.mRecordingProhibited;
+            mSeasonTitle = other.mSeasonTitle;
+            mExternalId = other.mInternalProviderId;
+            mPreviewVideoUri = other.mPreviewVideoUri;
+            mPreviewPosition = other.mPreviewLastPlaybackPosition;
+            mPreviewDuration = other.mPreviewDuration;
+            mPreviewAppLinkIntentUri = other.mPreviewAppLinkIntentUri;
+            mWeight = other.mPreviewWeight;
+            mTransient = other.mTransient;
+        }
+
+        /**
+         * Sets a unique id for this program.
+         *
+         * @param programId The value of {@link Programs#_ID} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setId(long programId) {
+            mId = programId;
+            return this;
+        }
+
+        /**
+         * Sets the ID of the {@link Channel} that contains this program.
+         *
+         * @param channelId The value of {@link Programs#COLUMN_CHANNEL_ID for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setChannelId(long channelId) {
+            mChannelId = channelId;
+            return this;
+        }
+
+        /**
+         * Sets the title of this program. For a series, this is the series title.
+         *
+         * @param title The value of {@link Programs#COLUMN_TITLE} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setTitle(String title) {
+            mTitle = title;
+            return this;
+        }
+
+        /**
+         * Sets the title of this particular episode for a series.
+         *
+         * @param episodeTitle The value of {@link Programs#COLUMN_EPISODE_TITLE} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setEpisodeTitle(String episodeTitle) {
+            mEpisodeTitle = episodeTitle;
+            return this;
+        }
+
+        /**
+         * Sets the season number for this episode for a series.
+         *
+         * @param seasonNumber The value of
+         * {@link Programs#COLUMN_SEASON_DISPLAY_NUMBER} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setSeasonNumber(int seasonNumber) {
+            mSeasonNumber = String.valueOf(seasonNumber);
+            return this;
+        }
+
+        /**
+         * Sets the season number for this episode for a series.
+         *
+         * @param seasonNumber The value of {@link Programs#COLUMN_SEASON_NUMBER} for the program.
+         * @param numericalSeasonNumber An integer value for {@link Programs#COLUMN_SEASON_NUMBER}
+         *                              which will be used for API Level 23 and below.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setSeasonNumber(String seasonNumber, int numericalSeasonNumber) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                mSeasonNumber = seasonNumber;
+            } else {
+                mSeasonNumber = String.valueOf(numericalSeasonNumber);
+            }
+            return this;
+        }
+
+        /**
+         * Sets the episode number in a season for this episode for a series.
+         *
+         * @param episodeNumber The value of
+         * {@link Programs#COLUMN_EPISODE_DISPLAY_NUMBER} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setEpisodeNumber(int episodeNumber) {
+            mEpisodeNumber = String.valueOf(episodeNumber);
+            return this;
+        }
+
+        /**
+         * Sets the episode number in a season for this episode for a series.
+         *
+         * @param episodeNumber The value of {@link Programs#COLUMN_EPISODE_DISPLAY_NUMBER} for the
+         *                      program.
+         * @param numericalEpisodeNumber An integer value for {@link Programs#COLUMN_SEASON_NUMBER}
+         *                               which will be used for API Level 23 and below.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setEpisodeNumber(String episodeNumber, int numericalEpisodeNumber) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                mEpisodeNumber = episodeNumber;
+            } else {
+                mEpisodeNumber = String.valueOf(numericalEpisodeNumber);
+            }
+            return this;
+        }
+
+        /**
+         * Sets the time when the program is going to begin in milliseconds since the epoch.
+         *
+         * @param startTimeUtcMillis The value of {@link Programs#COLUMN_START_TIME_UTC_MILLIS} for
+         *                           the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setStartTimeUtcMillis(long startTimeUtcMillis) {
+            mStartTimeUtcMillis = startTimeUtcMillis;
+            return this;
+        }
+
+        /**
+         * Sets the time when this program is going to end in milliseconds since the epoch.
+         *
+         * @param endTimeUtcMillis The value of {@link Programs#COLUMN_END_TIME_UTC_MILLIS} for the
+         *                         program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setEndTimeUtcMillis(long endTimeUtcMillis) {
+            mEndTimeUtcMillis = endTimeUtcMillis;
+            return this;
+        }
+
+        /**
+         * Sets a brief description of the program. For a series, this would be a brief description
+         * of the episode.
+         *
+         * @param description The value of {@link Programs#COLUMN_SHORT_DESCRIPTION} for the
+         *                    program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setDescription(String description) {
+            mDescription = description;
+            return this;
+        }
+
+        /**
+         * Sets a longer description of a program if one exists.
+         *
+         * @param longDescription The value of {@link Programs#COLUMN_LONG_DESCRIPTION} for the
+         *                        program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setLongDescription(String longDescription) {
+            mLongDescription = longDescription;
+            return this;
+        }
+
+        /**
+         * Sets the video width of the program.
+         *
+         * @param width The value of {@link Programs#COLUMN_VIDEO_WIDTH} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setVideoWidth(int width) {
+            mVideoWidth = width;
+            return this;
+        }
+
+        /**
+         * Sets the video height of the program.
+         *
+         * @param height The value of {@link Programs#COLUMN_VIDEO_HEIGHT} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setVideoHeight(int height) {
+            mVideoHeight = height;
+            return this;
+        }
+
+        /**
+         * Sets the content ratings for this program.
+         *
+         * @param contentRatings An array of {@link TvContentRating} that apply to this program
+         *                       which will be flattened to a String to store in a database.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         * @see Programs#COLUMN_CONTENT_RATING
+         */
+        public Builder setContentRatings(TvContentRating[] contentRatings) {
+            mContentRatings = contentRatings;
+            return this;
+        }
+
+        /**
+         * Sets the large poster art of the program.
+         *
+         * @param posterArtUri The value of {@link Programs#COLUMN_POSTER_ART_URI} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setPosterArtUri(String posterArtUri) {
+            mPosterArtUri = posterArtUri;
+            return this;
+        }
+
+        /**
+         * Sets a small thumbnail of the program.
+         *
+         * @param thumbnailUri The value of {@link Programs#COLUMN_THUMBNAIL_URI} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setThumbnailUri(String thumbnailUri) {
+            mThumbnailUri = thumbnailUri;
+            return this;
+        }
+
+        /**
+         * Sets the broadcast-specified genres of the program.
+         *
+         * @param genres Array of genres that apply to the program based on the broadcast standard
+         *               which will be flattened to a String to store in a database.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         * @see Programs#COLUMN_BROADCAST_GENRE
+         */
+        public Builder setBroadcastGenres(String[] genres) {
+            mBroadcastGenres = genres;
+            return this;
+        }
+
+        /**
+         * Sets the genres of the program.
+         *
+         * @param genres An array of {@link Programs.Genres} that apply to the program which will be
+         *               flattened to a String to store in a database.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         * @see Programs#COLUMN_CANONICAL_GENRE
+         */
+        public Builder setCanonicalGenres(String[] genres) {
+            mCanonicalGenres = genres;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider data for the program as raw bytes.
+         *
+         * @param data The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_DATA} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderData(byte[] data) {
+            mInternalProviderData = data;
+            return this;
+        }
+
+        /**
+         * Sets the available audio languages for this program as a comma-separated String.
+         *
+         * @param audioLanguages An array of audio languages, in ISO 639-1 or 639-2/T codes, that
+         *                       apply to this program which will be stored in a database.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setAudioLanguages(String[] audioLanguages) {
+            mAudioLanguages = audioLanguages;
+            return this;
+        }
+
+        /**
+         * Sets whether this channel can be searched for in other applications.
+         *
+         * @param searchable The value of {@link Programs#COLUMN_SEARCHABLE} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setSearchable(boolean searchable) {
+            mSearchable = searchable ? IS_SEARCHABLE : 0;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider flag1 for the program.
+         *
+         * @param flag The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_FLAG1} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderFlag1(long flag) {
+            mInternalProviderFlag1 = flag;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider flag2 for the program.
+         *
+         * @param flag The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_FLAG2} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderFlag2(long flag) {
+            mInternalProviderFlag2 = flag;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider flag3 for the program.
+         *
+         * @param flag The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_FLAG3} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderFlag3(long flag) {
+            mInternalProviderFlag3 = flag;
+            return this;
+        }
+
+        /**
+         * Sets the internal provider flag4 for the program.
+         *
+         * @param flag The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_FLAG4} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderFlag4(long flag) {
+            mInternalProviderFlag4 = flag;
+            return this;
+        }
+
+        /**
+         * Sets whether this program cannot be recorded.
+         *
+         * @param prohibited The value of {@link Programs#COLUMN_RECORDING_PROHIBITED} for the
+         *                   program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setRecordingProhibited(boolean prohibited) {
+            mRecordingProhibited = prohibited ? IS_RECORDING_PROHIBITED : 0;
+            return this;
+        }
+
+        /**
+         * Sets a custom name for the season, if applicable.
+         *
+         * @param seasonTitle The value of {@link Programs#COLUMN_SEASON_TITLE} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setSeasonTitle(String seasonTitle) {
+            mSeasonTitle = seasonTitle;
+            return this;
+        }
+
+        /**
+         * Sets external ID for the program.
+         *
+         * @param externalId The value of {@link Programs#COLUMN_INTERNAL_PROVIDER_ID} for the
+         *                   program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setInternalProviderId(String externalId) {
+            mExternalId = externalId;
+            return this;
+        }
+
+        /**
+         * Sets a URI for the preview video.
+         *
+         * @param previewVideoUri The value of {@link Programs#COLUMN_PREVIEW_VIDEO_URI} for the
+         *                        program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setPreviewVideoUri(String previewVideoUri) {
+            mPreviewVideoUri = previewVideoUri;
+            return this;
+        }
+
+        /**
+         * Sets the last playback position (in milliseconds) of the preview video.
+         *
+         * @param position The value of {@link Programs#COLUMN_PREVIEW_LAST_PLAYBACK_POSITION} for
+         *                 the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setPreviewLastPlaybackPosition(int position) {
+            mPreviewPosition = position;
+            return this;
+        }
+
+        /**
+         * Sets the last playback duration (in milliseconds) of the preview video.
+         *
+         * @param duration The value of {@link Programs#COLUMN_PREVIEW_DURATION} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setPreviewDuration(int duration) {
+            mPreviewDuration = duration;
+            return this;
+        }
+
+        /**
+         * Sets the intent URI of the app link for the preview video.
+         *
+         * @param previewAppLinkIntentUri The value of {@link Programs#COLUMN_PREVIEW_INTENT_URI}
+         *                                for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setPreviewIntentUri(String previewAppLinkIntentUri) {
+            mPreviewAppLinkIntentUri = previewAppLinkIntentUri;
+            return this;
+        }
+
+        /**
+         * Sets the weight of the preview program within the channel.
+         *
+         * @param weight The value of {@link Programs#COLUMN_PREVIEW_WEIGHT} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         */
+        public Builder setPreviewWeight(int weight) {
+            mWeight = weight;
+            return this;
+        }
+
+        /**
+         * Sets whether this program is transient or not.
+         *
+         * @param transientValue The value of {@link Programs#COLUMN_TRANSIENT} for the program.
+         * @return This Builder object to allow for chaining of calls to builder methods.
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public Builder setTransient(boolean transientValue) {
+            mTransient = transientValue ? IS_TRANSIENT : 0;
+            return this;
+        }
+
+        /**
+         * @return A new Program with values supplied by the Builder.
+         */
+        public Program build() {
+            return new Program(this);
+        }
+    }
+}
diff --git a/tv-provider/src/android/support/media/tv/TvContractCompat.java b/tv-provider/src/android/support/media/tv/TvContractCompat.java
new file mode 100644
index 0000000..4ff7030
--- /dev/null
+++ b/tv-provider/src/android/support/media/tv/TvContractCompat.java
@@ -0,0 +1,2091 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.tv;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.media.tv.TvContentRating;
+import android.media.tv.TvContract;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The contract between the TV provider and applications. Contains definitions for the supported
+ * URIs and columns.
+ * <h3>Overview</h3>
+ *
+ * <p>TvContract defines a basic database of TV content metadata such as channel and program
+ * information. The information is stored in {@link Channels} and {@link Programs} tables.
+ *
+ * <ul>
+ *     <li>A row in the {@link Channels} table represents information about a TV channel. The data
+ *         format can vary greatly from standard to standard or according to service provider, thus
+ *         the columns here are mostly comprised of basic entities that are usually seen to users
+ *         regardless of standard such as channel number and name.</li>
+ *     <li>A row in the {@link Programs} table represents a set of data describing a TV program such
+ *         as program title and start time.</li>
+ * </ul>
+ */
+public final class TvContractCompat {
+    /** The authority for the TV provider. */
+    public static final String AUTHORITY = "android.media.tv";
+
+    /**
+     * Permission to read TV listings. This is required to read all the TV channel and program
+     * information available on the system.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String PERMISSION_READ_TV_LISTINGS = "android.permission.READ_TV_LISTINGS";
+
+    private static final String PATH_CHANNEL = "channel";
+    private static final String PATH_PROGRAM = "program";
+    private static final String PATH_RECORDED_PROGRAM = "recorded_program";
+    private static final String PATH_PASSTHROUGH = "passthrough";
+
+    /**
+     * An optional query, update or delete URI parameter that allows the caller to specify TV input
+     * ID to filter channels.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String PARAM_INPUT = "input";
+
+    /**
+     * An optional query, update or delete URI parameter that allows the caller to specify channel
+     * ID to filter programs.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String PARAM_CHANNEL = "channel";
+
+    /**
+     * An optional query, update or delete URI parameter that allows the caller to specify start
+     * time (in milliseconds since the epoch) to filter programs.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String PARAM_START_TIME = "start_time";
+
+    /**
+     * An optional query, update or delete URI parameter that allows the caller to specify end time
+     * (in milliseconds since the epoch) to filter programs.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String PARAM_END_TIME = "end_time";
+
+    /**
+     * A query, update or delete URI parameter that allows the caller to operate on all or
+     * browsable-only channels. If set to "true", the rows that contain non-browsable channels are
+     * not affected.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String PARAM_BROWSABLE_ONLY = "browsable_only";
+
+    /**
+     * A optional query, update or delete URI parameter that allows the caller to specify canonical
+     * genre to filter programs.
+     * @hide
+     */
+    @RestrictTo(LIBRARY_GROUP)
+    public static final String PARAM_CANONICAL_GENRE = "canonical_genre";
+
+    /**
+     * Builds an ID that uniquely identifies a TV input service.
+     *
+     * @param name The {@link ComponentName} of the TV input service to build ID for.
+     * @return the ID for the given TV input service.
+     */
+    public static String buildInputId(ComponentName name) {
+        return TvContract.buildInputId(name);
+    }
+
+    /**
+     * Builds a URI that points to a specific channel.
+     *
+     * @param channelId The ID of the channel to point to.
+     */
+    public static Uri buildChannelUri(long channelId) {
+        return TvContract.buildChannelUri(channelId);
+    }
+
+    /**
+     * Build a special channel URI intended to be used with pass-through inputs. (e.g. HDMI)
+     *
+     * @param inputId The ID of the pass-through input to build a channels URI for.
+     */
+    public static Uri buildChannelUriForPassthroughInput(String inputId) {
+        return TvContract.buildChannelUriForPassthroughInput(inputId);
+    }
+
+    /**
+     * Builds a URI that points to a channel logo. See {@link Channels.Logo}.
+     *
+     * @param channelId The ID of the channel whose logo is pointed to.
+     */
+    public static Uri buildChannelLogoUri(long channelId) {
+        return TvContract.buildChannelLogoUri(channelId);
+    }
+
+    /**
+     * Builds a URI that points to a channel logo. See {@link Channels.Logo}.
+     *
+     * @param channelUri The URI of the channel whose logo is pointed to.
+     */
+    public static Uri buildChannelLogoUri(Uri channelUri) {
+        return TvContract.buildChannelLogoUri(channelUri);
+    }
+
+    /**
+     * Builds a URI that points to all channels from a given TV input.
+     *
+     * @param inputId The ID of the TV input to build a channels URI for. If {@code null}, builds a
+     *            URI for all the TV inputs.
+     */
+    public static Uri buildChannelsUriForInput(@Nullable String inputId) {
+        return TvContract.buildChannelsUriForInput(inputId);
+    }
+
+    /**
+     * Builds a URI that points to a specific program.
+     *
+     * @param programId The ID of the program to point to.
+     */
+    public static Uri buildProgramUri(long programId) {
+        return TvContract.buildProgramUri(programId);
+    }
+
+    /**
+     * Builds a URI that points to all programs on a given channel.
+     *
+     * @param channelId The ID of the channel to return programs for.
+     */
+    public static Uri buildProgramsUriForChannel(long channelId) {
+        return TvContract.buildProgramsUriForChannel(channelId);
+    }
+
+    /**
+     * Builds a URI that points to all programs on a given channel.
+     *
+     * @param channelUri The URI of the channel to return programs for.
+     */
+    public static Uri buildProgramsUriForChannel(Uri channelUri) {
+        return TvContract.buildProgramsUriForChannel(channelUri);
+    }
+
+    /**
+     * Builds a URI that points to programs on a specific channel whose schedules overlap with the
+     * given time frame.
+     *
+     * @param channelId The ID of the channel to return programs for.
+     * @param startTime The start time used to filter programs. The returned programs should have
+     *            {@link Programs#COLUMN_END_TIME_UTC_MILLIS} that is greater than this time.
+     * @param endTime The end time used to filter programs. The returned programs should have
+     *            {@link Programs#COLUMN_START_TIME_UTC_MILLIS} that is less than this time.
+     */
+    public static Uri buildProgramsUriForChannel(long channelId, long startTime,
+            long endTime) {
+        return TvContract.buildProgramsUriForChannel(channelId, startTime, endTime);
+    }
+
+    /**
+     * Builds a URI that points to programs on a specific channel whose schedules overlap with the
+     * given time frame.
+     *
+     * @param channelUri The URI of the channel to return programs for.
+     * @param startTime The start time used to filter programs. The returned programs should have
+     *            {@link Programs#COLUMN_END_TIME_UTC_MILLIS} that is greater than this time.
+     * @param endTime The end time used to filter programs. The returned programs should have
+     *            {@link Programs#COLUMN_START_TIME_UTC_MILLIS} that is less than this time.
+     */
+    public static Uri buildProgramsUriForChannel(Uri channelUri, long startTime,
+            long endTime) {
+        return TvContract.buildProgramsUriForChannel(channelUri, startTime, endTime);
+    }
+
+    /**
+     * Builds a URI that points to a specific recorded program.
+     *
+     * @param recordedProgramId The ID of the recorded program to point to.
+     */
+    public static Uri buildRecordedProgramUri(long recordedProgramId) {
+        if (android.os.Build.VERSION.SDK_INT >= 24) {
+            return TvContract.buildRecordedProgramUri(recordedProgramId);
+        } else {
+            return ContentUris.withAppendedId(RecordedPrograms.CONTENT_URI, recordedProgramId);
+        }
+    }
+
+    private static boolean isTvUri(Uri uri) {
+        return uri != null && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())
+                && AUTHORITY.equals(uri.getAuthority());
+    }
+
+    private static boolean isTwoSegmentUriStartingWith(Uri uri, String pathSegment) {
+        List<String> pathSegments = uri.getPathSegments();
+        return pathSegments.size() == 2 && pathSegment.equals(pathSegments.get(0));
+    }
+
+    /**
+     * Returns {@code true}, if {@code uri} is a channel URI.
+     */
+    public static boolean isChannelUri(Uri uri) {
+        if (android.os.Build.VERSION.SDK_INT >= 24) {
+            return TvContract.isChannelUri(uri);
+        } else {
+            return isChannelUriForTunerInput(uri) || isChannelUriForPassthroughInput(uri);
+        }
+    }
+
+    /**
+     * Returns {@code true}, if {@code uri} is a channel URI for a tuner input.
+     */
+    public static boolean isChannelUriForTunerInput(Uri uri) {
+        if (android.os.Build.VERSION.SDK_INT >= 24) {
+            return TvContract.isChannelUriForTunerInput(uri);
+        } else {
+            return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_CHANNEL);
+        }
+    }
+
+    /**
+     * Returns {@code true}, if {@code uri} is a channel URI for a pass-through input.
+     */
+    public static boolean isChannelUriForPassthroughInput(Uri uri) {
+        if (android.os.Build.VERSION.SDK_INT >= 24) {
+            return TvContract.isChannelUriForPassthroughInput(uri);
+        } else {
+            return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_PASSTHROUGH);
+        }
+    }
+
+    /**
+     * Returns {@code true}, if {@code uri} is a program URI.
+     */
+    public static boolean isProgramUri(Uri uri) {
+        if (android.os.Build.VERSION.SDK_INT >= 24) {
+            return TvContract.isProgramUri(uri);
+        } else {
+            return isTvUri(uri) && isTwoSegmentUriStartingWith(uri, PATH_PROGRAM);
+        }
+    }
+
+
+    private TvContractCompat() {}
+
+    /**
+     * Common base for the tables of TV channels/programs.
+     */
+    public interface BaseTvColumns extends BaseColumns {
+        /**
+         * The name of the package that owns the current row.
+         *
+         * <p>The TV provider fills in this column with the name of the package that provides the
+         * initial data of the row. If the package is later uninstalled, the rows it owns are
+         * automatically removed from the tables.
+         *
+         * <p>Type: TEXT
+         */
+        String COLUMN_PACKAGE_NAME = "package_name";
+    }
+
+    /** Column definitions for the TV channels table. */
+    public static final class Channels implements BaseTvColumns {
+
+        /** The content:// style URI for this table. */
+        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/"
+                + PATH_CHANNEL);
+
+        /** The MIME type of a directory of TV channels. */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/channel";
+
+        /** The MIME type of a single TV channel. */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/channel";
+
+        /**
+         * A generic channel type.
+         *
+         * Use this if the current channel is streaming-based or its broadcast system type does not
+         * fit under any other types. This is the default channel type.
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_OTHER = "TYPE_OTHER";
+
+        /**
+         * The channel type for NTSC.
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_NTSC = "TYPE_NTSC";
+
+        /**
+         * The channel type for PAL.
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_PAL = "TYPE_PAL";
+
+        /**
+         * The channel type for SECAM.
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_SECAM = "TYPE_SECAM";
+
+        /**
+         * The channel type for DVB-T (terrestrial).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_DVB_T = "TYPE_DVB_T";
+
+        /**
+         * The channel type for DVB-T2 (terrestrial).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_DVB_T2 = "TYPE_DVB_T2";
+
+        /**
+         * The channel type for DVB-S (satellite).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_DVB_S = "TYPE_DVB_S";
+
+        /**
+         * The channel type for DVB-S2 (satellite).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_DVB_S2 = "TYPE_DVB_S2";
+
+        /**
+         * The channel type for DVB-C (cable).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_DVB_C = "TYPE_DVB_C";
+
+        /**
+         * The channel type for DVB-C2 (cable).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_DVB_C2 = "TYPE_DVB_C2";
+
+        /**
+         * The channel type for DVB-H (handheld).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_DVB_H = "TYPE_DVB_H";
+
+        /**
+         * The channel type for DVB-SH (satellite).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_DVB_SH = "TYPE_DVB_SH";
+
+        /**
+         * The channel type for ATSC (terrestrial).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_ATSC_T = "TYPE_ATSC_T";
+
+        /**
+         * The channel type for ATSC (cable).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_ATSC_C = "TYPE_ATSC_C";
+
+        /**
+         * The channel type for ATSC-M/H (mobile/handheld).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_ATSC_M_H = "TYPE_ATSC_M_H";
+
+        /**
+         * The channel type for ISDB-T (terrestrial).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_ISDB_T = "TYPE_ISDB_T";
+
+        /**
+         * The channel type for ISDB-Tb (Brazil).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_ISDB_TB = "TYPE_ISDB_TB";
+
+        /**
+         * The channel type for ISDB-S (satellite).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_ISDB_S = "TYPE_ISDB_S";
+
+        /**
+         * The channel type for ISDB-C (cable).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_ISDB_C = "TYPE_ISDB_C";
+
+        /**
+         * The channel type for 1seg (handheld).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_1SEG = "TYPE_1SEG";
+
+        /**
+         * The channel type for DTMB (terrestrial).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_DTMB = "TYPE_DTMB";
+
+        /**
+         * The channel type for CMMB (handheld).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_CMMB = "TYPE_CMMB";
+
+        /**
+         * The channel type for T-DMB (terrestrial).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_T_DMB = "TYPE_T_DMB";
+
+        /**
+         * The channel type for S-DMB (satellite).
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_S_DMB = "TYPE_S_DMB";
+
+        /**
+         * The channel type for preview videos.
+         *
+         * <P>Unlike other broadcast TV channel types, the programs in the preview channel usually
+         * are promotional videos. The UI may treat the preview channels differently from the other
+         * broadcast channels.
+         *
+         * @see #COLUMN_TYPE
+         */
+        public static final String TYPE_PREVIEW = "TYPE_PREVIEW";
+
+        /** A generic service type. */
+        public static final String SERVICE_TYPE_OTHER = "SERVICE_TYPE_OTHER";
+
+        /** The service type for regular TV channels that have both audio and video. */
+        public static final String SERVICE_TYPE_AUDIO_VIDEO = "SERVICE_TYPE_AUDIO_VIDEO";
+
+        /** The service type for radio channels that have audio only. */
+        public static final String SERVICE_TYPE_AUDIO = "SERVICE_TYPE_AUDIO";
+
+        /** The video format for 240p. */
+        public static final String VIDEO_FORMAT_240P = "VIDEO_FORMAT_240P";
+
+        /** The video format for 360p. */
+        public static final String VIDEO_FORMAT_360P = "VIDEO_FORMAT_360P";
+
+        /** The video format for 480i. */
+        public static final String VIDEO_FORMAT_480I = "VIDEO_FORMAT_480I";
+
+        /** The video format for 480p. */
+        public static final String VIDEO_FORMAT_480P = "VIDEO_FORMAT_480P";
+
+        /** The video format for 576i. */
+        public static final String VIDEO_FORMAT_576I = "VIDEO_FORMAT_576I";
+
+        /** The video format for 576p. */
+        public static final String VIDEO_FORMAT_576P = "VIDEO_FORMAT_576P";
+
+        /** The video format for 720p. */
+        public static final String VIDEO_FORMAT_720P = "VIDEO_FORMAT_720P";
+
+        /** The video format for 1080i. */
+        public static final String VIDEO_FORMAT_1080I = "VIDEO_FORMAT_1080I";
+
+        /** The video format for 1080p. */
+        public static final String VIDEO_FORMAT_1080P = "VIDEO_FORMAT_1080P";
+
+        /** The video format for 2160p. */
+        public static final String VIDEO_FORMAT_2160P = "VIDEO_FORMAT_2160P";
+
+        /** The video format for 4320p. */
+        public static final String VIDEO_FORMAT_4320P = "VIDEO_FORMAT_4320P";
+
+        /** The video resolution for standard-definition. */
+        public static final String VIDEO_RESOLUTION_SD = "VIDEO_RESOLUTION_SD";
+
+        /** The video resolution for enhanced-definition. */
+        public static final String VIDEO_RESOLUTION_ED = "VIDEO_RESOLUTION_ED";
+
+        /** The video resolution for high-definition. */
+        public static final String VIDEO_RESOLUTION_HD = "VIDEO_RESOLUTION_HD";
+
+        /** The video resolution for full high-definition. */
+        public static final String VIDEO_RESOLUTION_FHD = "VIDEO_RESOLUTION_FHD";
+
+        /** The video resolution for ultra high-definition. */
+        public static final String VIDEO_RESOLUTION_UHD = "VIDEO_RESOLUTION_UHD";
+
+        private static final Map<String, String> VIDEO_FORMAT_TO_RESOLUTION_MAP = new HashMap<>();
+
+        static {
+            VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_480I, VIDEO_RESOLUTION_SD);
+            VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_480P, VIDEO_RESOLUTION_ED);
+            VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_576I, VIDEO_RESOLUTION_SD);
+            VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_576P, VIDEO_RESOLUTION_ED);
+            VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_720P, VIDEO_RESOLUTION_HD);
+            VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_1080I, VIDEO_RESOLUTION_HD);
+            VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_1080P, VIDEO_RESOLUTION_FHD);
+            VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_2160P, VIDEO_RESOLUTION_UHD);
+            VIDEO_FORMAT_TO_RESOLUTION_MAP.put(VIDEO_FORMAT_4320P, VIDEO_RESOLUTION_UHD);
+        }
+
+        /**
+         * Returns the video resolution (definition) for a given video format.
+         *
+         * @param videoFormat The video format defined in {@link Channels}.
+         * @return the corresponding video resolution string. {@code null} if the resolution string
+         *         is not defined for the given video format.
+         * @see #COLUMN_VIDEO_FORMAT
+         */
+        @Nullable
+        public static String getVideoResolution(String videoFormat) {
+            return VIDEO_FORMAT_TO_RESOLUTION_MAP.get(videoFormat);
+        }
+
+        /**
+         * The ID of the TV input service that provides this TV channel.
+         *
+         * <p>Use {@link #buildInputId} to build the ID.
+         *
+         * <p>This is a required field.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_INPUT_ID = "input_id";
+
+        /**
+         * The broadcast system type of this TV channel.
+         *
+         * <p>This is used to indicate the broadcast standard (e.g. ATSC, DVB or ISDB) the current
+         * channel conforms to. Use {@link #TYPE_OTHER} for streaming-based channels, which is the
+         * default channel type. The value should match to one of the followings:
+         * {@link #TYPE_1SEG},
+         * {@link #TYPE_ATSC_C},
+         * {@link #TYPE_ATSC_M_H},
+         * {@link #TYPE_ATSC_T},
+         * {@link #TYPE_CMMB},
+         * {@link #TYPE_DTMB},
+         * {@link #TYPE_DVB_C},
+         * {@link #TYPE_DVB_C2},
+         * {@link #TYPE_DVB_H},
+         * {@link #TYPE_DVB_S},
+         * {@link #TYPE_DVB_S2},
+         * {@link #TYPE_DVB_SH},
+         * {@link #TYPE_DVB_T},
+         * {@link #TYPE_DVB_T2},
+         * {@link #TYPE_ISDB_C},
+         * {@link #TYPE_ISDB_S},
+         * {@link #TYPE_ISDB_T},
+         * {@link #TYPE_ISDB_TB},
+         * {@link #TYPE_NTSC},
+         * {@link #TYPE_OTHER},
+         * {@link #TYPE_PAL},
+         * {@link #TYPE_SECAM},
+         * {@link #TYPE_S_DMB}, and
+         * {@link #TYPE_T_DMB}.
+         *
+         * <p>This is a required field.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_TYPE = "type";
+
+        /**
+         * The predefined service type of this TV channel.
+         *
+         * <p>This is primarily used to indicate whether the current channel is a regular TV channel
+         * or a radio-like channel. Use the same coding for {@code service_type} in the underlying
+         * broadcast standard if it is defined there (e.g. ATSC A/53, ETSI EN 300 468 and ARIB
+         * STD-B10). Otherwise use one of the followings: {@link #SERVICE_TYPE_OTHER},
+         * {@link #SERVICE_TYPE_AUDIO_VIDEO}, {@link #SERVICE_TYPE_AUDIO}
+         *
+         * <p>This is a required field.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_SERVICE_TYPE = "service_type";
+
+        /**
+         * The original network ID of this TV channel.
+         *
+         * <p>It is used to identify the originating delivery system, if applicable. Use the same
+         * coding for {@code original_network_id} for ETSI EN 300 468/TR 101 211 and ARIB STD-B10.
+         *
+         * <p>This is a required field only if the underlying broadcast standard defines the same
+         * name field. Otherwise, leave empty.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_ORIGINAL_NETWORK_ID = "original_network_id";
+
+        /**
+         * The transport stream ID of this channel.
+         *
+         * <p>It is used to identify the Transport Stream that contains the current channel from any
+         * other multiplex within a network, if applicable. Use the same coding for
+         * {@code transport_stream_id} defined in ISO/IEC 13818-1 if the channel is transmitted via
+         * the MPEG Transport Stream.
+         *
+         * <p>This is a required field only if the current channel is transmitted via the MPEG
+         * Transport Stream. Leave empty otherwise.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_TRANSPORT_STREAM_ID = "transport_stream_id";
+
+        /**
+         * The service ID of this channel.
+         *
+         * <p>It is used to identify the current service, or channel from any other services within
+         * a given Transport Stream, if applicable. Use the same coding for {@code service_id} in
+         * ETSI EN 300 468 and ARIB STD-B10 or {@code program_number} in ISO/IEC 13818-1.
+         *
+         * <p>This is a required field only if the underlying broadcast standard defines the same
+         * name field, or the current channel is transmitted via the MPEG Transport Stream. Leave
+         * empty otherwise.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_SERVICE_ID = "service_id";
+
+        /**
+         * The channel number that is displayed to the user.
+         *
+         * <p>The format can vary depending on broadcast standard and product specification.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_DISPLAY_NUMBER = "display_number";
+
+        /**
+         * The channel name that is displayed to the user.
+         *
+         * <p>A call sign is a good candidate to use for this purpose but any name that helps the
+         * user recognize the current channel will be enough. Can also be empty depending on
+         * broadcast standard.
+         *
+         * <p> Type: TEXT
+         */
+        public static final String COLUMN_DISPLAY_NAME = "display_name";
+
+        /**
+         * The network affiliation for this TV channel.
+         *
+         * <p>This is used to identify a channel that is commonly called by its network affiliation
+         * instead of the display name. Examples include ABC for the channel KGO-HD, FOX for the
+         * channel KTVU-HD and NBC for the channel KNTV-HD. Can be empty if not applicable.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_NETWORK_AFFILIATION = "network_affiliation";
+
+        /**
+         * The description of this TV channel.
+         *
+         * <p>Can be empty initially.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_DESCRIPTION = "description";
+
+        /**
+         * The typical video format for programs from this TV channel.
+         *
+         * <p>This is primarily used to filter out channels based on video format by applications.
+         * The value should match one of the followings: {@link #VIDEO_FORMAT_240P},
+         * {@link #VIDEO_FORMAT_360P}, {@link #VIDEO_FORMAT_480I}, {@link #VIDEO_FORMAT_480P},
+         * {@link #VIDEO_FORMAT_576I}, {@link #VIDEO_FORMAT_576P}, {@link #VIDEO_FORMAT_720P},
+         * {@link #VIDEO_FORMAT_1080I}, {@link #VIDEO_FORMAT_1080P}, {@link #VIDEO_FORMAT_2160P},
+         * {@link #VIDEO_FORMAT_4320P}. Note that the actual video resolution of each program from a
+         * given channel can vary thus one should use {@link Programs#COLUMN_VIDEO_WIDTH} and
+         * {@link Programs#COLUMN_VIDEO_HEIGHT} to get more accurate video resolution.
+         *
+         * <p>Type: TEXT
+         *
+         * @see #getVideoResolution
+         */
+        public static final String COLUMN_VIDEO_FORMAT = "video_format";
+
+        /**
+         * The flag indicating whether this TV channel is browsable or not.
+         *
+         * <p>A value of 1 indicates the channel is included in the channel list that applications
+         * use to browse channels, a value of 0 indicates the channel is not included in the list.
+         * If not specified, this value is set to 0 (not browsable) by default.
+         *
+         * <p>Type: INTEGER (boolean)
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public static final String COLUMN_BROWSABLE = "browsable";
+
+        /**
+         * The flag indicating whether this TV channel is searchable or not.
+         *
+         * <p>The columns of searchable channels can be read by other applications that have proper
+         * permission. Care must be taken not to open sensitive data.
+         *
+         * <p>A value of 1 indicates that the channel is searchable and its columns can be read by
+         * other applications, a value of 0 indicates that the channel is hidden and its columns can
+         * be read only by the package that owns the channel and the system. If not specified, this
+         * value is set to 1 (searchable) by default.
+         *
+         * <p>Type: INTEGER (boolean)
+         */
+        public static final String COLUMN_SEARCHABLE = "searchable";
+
+        /**
+         * The flag indicating whether this TV channel is locked or not.
+         *
+         * <p>This is primarily used for alternative parental control to prevent unauthorized users
+         * from watching the current channel regardless of the content rating. A value of 1
+         * indicates the channel is locked and the user is required to enter passcode to unlock it
+         * in order to watch the current program from the channel, a value of 0 indicates the
+         * channel is not locked thus the user is not prompted to enter passcode If not specified,
+         * this value is set to 0 (not locked) by default.
+         *
+         * <p>Type: INTEGER (boolean)
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public static final String COLUMN_LOCKED = "locked";
+
+        /**
+         * The URI for the app badge icon of the app link template for this channel.
+         *
+         * <p>This small icon is overlaid at the bottom of the poster art specified by
+         * {@link #COLUMN_APP_LINK_POSTER_ART_URI}. The data in the column must be a URI in one of
+         * the following formats:
+         *
+         * <ul>
+         * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+         * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+         * </li>
+         * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+         * </ul>
+         *
+         * <p>The app-linking allows channel input sources to provide activity links from their live
+         * channel programming to another activity. This enables content providers to increase user
+         * engagement by offering the viewer other content or actions.
+         *
+         * <p>Type: TEXT
+         * @see #COLUMN_APP_LINK_COLOR
+         * @see #COLUMN_APP_LINK_INTENT_URI
+         * @see #COLUMN_APP_LINK_POSTER_ART_URI
+         * @see #COLUMN_APP_LINK_TEXT
+         */
+        public static final String COLUMN_APP_LINK_ICON_URI = "app_link_icon_uri";
+
+        /**
+         * The URI for the poster art used as the background of the app link template for this
+         * channel.
+         *
+         * <p>The data in the column must be a URL, or a URI in one of the following formats:
+         *
+         * <ul>
+         * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+         * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+         * </li>
+         * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+         * </ul>
+         *
+         * <p>The app-linking allows channel input sources to provide activity links from their live
+         * channel programming to another activity. This enables content providers to increase user
+         * engagement by offering the viewer other content or actions.
+         *
+         * <p>Type: TEXT
+         * @see #COLUMN_APP_LINK_COLOR
+         * @see #COLUMN_APP_LINK_ICON_URI
+         * @see #COLUMN_APP_LINK_INTENT_URI
+         * @see #COLUMN_APP_LINK_TEXT
+         */
+        public static final String COLUMN_APP_LINK_POSTER_ART_URI = "app_link_poster_art_uri";
+
+        /**
+         * The link text of the app link template for this channel.
+         *
+         * <p>This provides a short description of the action that happens when the corresponding
+         * app link is clicked.
+         *
+         * <p>The app-linking allows channel input sources to provide activity links from their live
+         * channel programming to another activity. This enables content providers to increase user
+         * engagement by offering the viewer other content or actions.
+         *
+         * <p>Type: TEXT
+         * @see #COLUMN_APP_LINK_COLOR
+         * @see #COLUMN_APP_LINK_ICON_URI
+         * @see #COLUMN_APP_LINK_INTENT_URI
+         * @see #COLUMN_APP_LINK_POSTER_ART_URI
+         */
+        public static final String COLUMN_APP_LINK_TEXT = "app_link_text";
+
+        /**
+         * The accent color of the app link template for this channel. This is primarily used for
+         * the background color of the text box in the template.
+         *
+         * <p>The app-linking allows channel input sources to provide activity links from their live
+         * channel programming to another activity. This enables content providers to increase user
+         * engagement by offering the viewer other content or actions.
+         *
+         * <p>Type: INTEGER (color value)
+         * @see #COLUMN_APP_LINK_ICON_URI
+         * @see #COLUMN_APP_LINK_INTENT_URI
+         * @see #COLUMN_APP_LINK_POSTER_ART_URI
+         * @see #COLUMN_APP_LINK_TEXT
+         */
+        public static final String COLUMN_APP_LINK_COLOR = "app_link_color";
+
+        /**
+         * The intent URI of the app link for this channel.
+         *
+         * <p>The URI is created using {@link Intent#toUri} with {@link Intent#URI_INTENT_SCHEME}
+         * and converted back to the original intent with {@link Intent#parseUri}. The intent is
+         * launched when the user clicks the corresponding app link for the current channel.
+         *
+         * <p>The app-linking allows channel input sources to provide activity links from their live
+         * channel programming to another activity. This enables content providers to increase user
+         * engagement by offering the viewer other content or actions.
+         *
+         * <p>Type: TEXT
+         * @see #COLUMN_APP_LINK_COLOR
+         * @see #COLUMN_APP_LINK_ICON_URI
+         * @see #COLUMN_APP_LINK_POSTER_ART_URI
+         * @see #COLUMN_APP_LINK_TEXT
+         */
+        public static final String COLUMN_APP_LINK_INTENT_URI = "app_link_intent_uri";
+
+        /**
+         * Internal data used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: BLOB
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data";
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG1 = "internal_provider_flag1";
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG2 = "internal_provider_flag2";
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG3 = "internal_provider_flag3";
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG4 = "internal_provider_flag4";
+
+        /**
+         * The version number of this row entry used by TV input services.
+         *
+         * <p>This is best used by sync adapters to identify the rows to update. The number can be
+         * defined by individual TV input services. One may assign the same value as
+         * {@code version_number} that appears in ETSI EN 300 468 or ATSC A/65, if the data are
+         * coming from a TV broadcast.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_VERSION_NUMBER = "version_number";
+
+        /**
+         * The flag indicating whether this TV channel is transient or not.
+         *
+         * <p>A value of 1 indicates that the channel will be automatically removed by the system on
+         * reboot, and a value of 0 indicates that the channel is persistent across reboot. If not
+         * specified, this value is set to 0 (not transient) by default.
+         *
+         * <p>Type: INTEGER (boolean)
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public static final String COLUMN_TRANSIENT = "transient";
+
+        private Channels() {}
+
+        /**
+         * A sub-directory of a single TV channel that represents its primary logo.
+         *
+         * <p>To access this directory, append {@link Channels.Logo#CONTENT_DIRECTORY} to the raw
+         * channel URI.  The resulting URI represents an image file, and should be interacted
+         * using ContentResolver.openAssetFileDescriptor.
+         *
+         * <p>Note that this sub-directory also supports opening the logo as an asset file in write
+         * mode.  Callers can create or replace the primary logo associated with this channel by
+         * opening the asset file and writing the full-size photo contents into it. (Make sure there
+         * is no padding around the logo image.) When the file is closed, the image will be parsed,
+         * sized down if necessary, and stored.
+         *
+         * <p>Usage example:
+         * <pre>
+         * public void writeChannelLogo(long channelId, byte[] logo) {
+         *     Uri channelLogoUri = TvContract.buildChannelLogoUri(channelId);
+         *     try {
+         *         AssetFileDescriptor fd =
+         *             getContentResolver().openAssetFileDescriptor(channelLogoUri, "rw");
+         *         OutputStream os = fd.createOutputStream();
+         *         os.write(logo);
+         *         os.close();
+         *         fd.close();
+         *     } catch (IOException e) {
+         *         // Handle error cases.
+         *     }
+         * }
+         * </pre>
+         */
+        public static final class Logo {
+
+            /**
+             * The directory twig for this sub-table.
+             */
+            public static final String CONTENT_DIRECTORY = "logo";
+
+            private Logo() {}
+        }
+    }
+
+    /**
+     * Column definitions for the TV programs table.
+     *
+     * <p>By default, the query results will be sorted by
+     * {@link Programs#COLUMN_START_TIME_UTC_MILLIS} in ascending order.
+     */
+    public static final class Programs implements BaseTvColumns {
+
+        /** The content:// style URI for this table. */
+        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/"
+                + PATH_PROGRAM);
+
+        /** The MIME type of a directory of TV programs. */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/program";
+
+        /** The MIME type of a single TV program. */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/program";
+
+        /**
+         * The ID of the TV channel that provides this TV program.
+         *
+         * <p>This is a part of the channel URI and matches to {@link BaseColumns#_ID}.
+         *
+         * <p>This is a required field.
+         *
+         * <p>Type: INTEGER (long)
+         */
+        public static final String COLUMN_CHANNEL_ID = "channel_id";
+
+        /**
+         * The title of this TV program.
+         *
+         * <p>If this program is an episodic TV show, it is recommended that the title is the series
+         * title and its related fields ({@link #COLUMN_SEASON_TITLE} and/or
+         * {@link #COLUMN_SEASON_DISPLAY_NUMBER}, {@link #COLUMN_SEASON_DISPLAY_NUMBER},
+         * {@link #COLUMN_EPISODE_DISPLAY_NUMBER}, and {@link #COLUMN_EPISODE_TITLE}) are filled in.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_TITLE = "title";
+
+        /**
+         * The season number of this TV program for episodic TV shows.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: INTEGER
+         *
+         * @deprecated Use {@link #COLUMN_SEASON_DISPLAY_NUMBER} instead.
+         */
+        @Deprecated
+        public static final String COLUMN_SEASON_NUMBER = "season_number";
+
+        /**
+         * The season display number of this TV program for episodic TV shows.
+         *
+         * <p>This is used to indicate the season number. (e.g. 1, 2 or 3) Note that the value
+         * does not necessarily be numeric. (e.g. 12B)
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_SEASON_DISPLAY_NUMBER = "season_display_number";
+
+        /**
+         * The title of the season for this TV program for episodic TV shows.
+         *
+         * <p>This is an optional field supplied only when the season has a special title
+         * (e.g. The Final Season). If provided, the applications should display it instead of
+         * {@link #COLUMN_SEASON_DISPLAY_NUMBER}, and should display it without alterations.
+         * (e.g. for "The Final Season", displayed string should be "The Final Season", not
+         * "Season The Final Season"). When displaying multiple programs, the order should be based
+         * on {@link #COLUMN_SEASON_DISPLAY_NUMBER}, even when {@link #COLUMN_SEASON_TITLE} exists.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_SEASON_TITLE = "season_title";
+
+        /**
+         * The episode number of this TV program for episodic TV shows.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: INTEGER
+         *
+         * @deprecated Use {@link #COLUMN_EPISODE_DISPLAY_NUMBER} instead.
+         */
+        @Deprecated
+        public static final String COLUMN_EPISODE_NUMBER = "episode_number";
+
+        /**
+         * The episode display number of this TV program for episodic TV shows.
+         *
+         * <p>This is used to indicate the episode number. (e.g. 1, 2 or 3) Note that the value
+         * does not necessarily be numeric. (e.g. 12B)
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_EPISODE_DISPLAY_NUMBER = "episode_display_number";
+
+        /**
+         * The episode title of this TV program for episodic TV shows.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_EPISODE_TITLE = "episode_title";
+
+        /**
+         * The start time of this TV program, in milliseconds since the epoch.
+         *
+         * <p>The value should be equal to or larger than {@link #COLUMN_END_TIME_UTC_MILLIS} of the
+         * previous program in the same channel. In practice, start time will usually be the end
+         * time of the previous program.
+         *
+         * <p>Can be empty if this program belongs to a {@link Channels#TYPE_PREVIEW} channel.
+         *
+         * <p>Type: INTEGER (long)
+         */
+        public static final String COLUMN_START_TIME_UTC_MILLIS = "start_time_utc_millis";
+
+        /**
+         * The end time of this TV program, in milliseconds since the epoch.
+         *
+         * <p>The value should be equal to or less than {@link #COLUMN_START_TIME_UTC_MILLIS} of the
+         * next program in the same channel. In practice, end time will usually be the start time of
+         * the next program.
+         *
+         * <p>Can be empty if this program belongs to a {@link Channels#TYPE_PREVIEW} channel.
+         *
+         * <p>Type: INTEGER (long)
+         */
+        public static final String COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis";
+
+        /**
+         * The comma-separated genre string of this TV program.
+         *
+         * <p>Use the same language appeared in the underlying broadcast standard, if applicable.
+         * (For example, one can refer to the genre strings used in Genre Descriptor of ATSC A/65 or
+         * Content Descriptor of ETSI EN 300 468, if appropriate.) Otherwise, leave empty. Use
+         * {@link Genres#encode} to create a text that can be stored in this column. Use
+         * {@link Genres#decode} to get the broadcast genre strings from the text stored in the
+         * column.
+         *
+         * <p>Type: TEXT
+         * @see Genres#encode
+         * @see Genres#decode
+         */
+        public static final String COLUMN_BROADCAST_GENRE = "broadcast_genre";
+
+        /**
+         * The comma-separated canonical genre string of this TV program.
+         *
+         * <p>Canonical genres are defined in {@link Genres}. Use {@link Genres#encode} to create a
+         * text that can be stored in this column. Use {@link Genres#decode} to get the canonical
+         * genre strings from the text stored in the column.
+         *
+         * <p>Type: TEXT
+         * @see Genres
+         * @see Genres#encode
+         * @see Genres#decode
+         */
+        public static final String COLUMN_CANONICAL_GENRE = "canonical_genre";
+
+        /**
+         * The short description of this TV program that is displayed to the user by default.
+         *
+         * <p>It is recommended to limit the length of the descriptions to 256 characters.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_SHORT_DESCRIPTION = "short_description";
+
+        /**
+         * The detailed, lengthy description of this TV program that is displayed only when the user
+         * wants to see more information.
+         *
+         * <p>TV input services should leave this field empty if they have no additional details
+         * beyond {@link #COLUMN_SHORT_DESCRIPTION}.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_LONG_DESCRIPTION = "long_description";
+
+        /**
+         * The width of the video for this TV program, in the unit of pixels.
+         *
+         * <p>Together with {@link #COLUMN_VIDEO_HEIGHT} this is used to determine the video
+         * resolution of the current TV program. Can be empty if it is not known initially or the
+         * program does not convey any video such as the programs from type
+         * {@link Channels#SERVICE_TYPE_AUDIO} channels.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_VIDEO_WIDTH = "video_width";
+
+        /**
+         * The height of the video for this TV program, in the unit of pixels.
+         *
+         * <p>Together with {@link #COLUMN_VIDEO_WIDTH} this is used to determine the video
+         * resolution of the current TV program. Can be empty if it is not known initially or the
+         * program does not convey any video such as the programs from type
+         * {@link Channels#SERVICE_TYPE_AUDIO} channels.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_VIDEO_HEIGHT = "video_height";
+
+        /**
+         * The comma-separated audio languages of this TV program.
+         *
+         * <p>This is used to describe available audio languages included in the program. Use either
+         * ISO 639-1 or 639-2/T codes.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_AUDIO_LANGUAGE = "audio_language";
+
+        /**
+         * The comma-separated content ratings of this TV program.
+         *
+         * <p>This is used to describe the content rating(s) of this program. Each comma-separated
+         * content rating sub-string should be generated by calling
+         * {@link TvContentRating#flattenToString}. Note that in most cases the program content is
+         * rated by a single rating system, thus resulting in a corresponding single sub-string that
+         * does not require comma separation and multiple sub-strings appear only when the program
+         * content is rated by two or more content rating systems. If any of those ratings is
+         * specified as "blocked rating" in the user's parental control settings, the TV input
+         * service should block the current content and wait for the signal that it is okay to
+         * unblock.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_CONTENT_RATING = "content_rating";
+
+        /**
+         * The URI for the poster art of this TV program.
+         *
+         * <p>The data in the column must be a URL, or a URI in one of the following formats:
+         *
+         * <ul>
+         * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+         * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+         * </li>
+         * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+         * </ul>
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_POSTER_ART_URI = "poster_art_uri";
+
+        /**
+         * The URI for the thumbnail of this TV program.
+         *
+         * <p>The system can generate a thumbnail from the poster art if this column is not
+         * specified. Thus it is not necessary for TV input services to include a thumbnail if it is
+         * just a scaled image of the poster art.
+         *
+         * <p>The data in the column must be a URL, or a URI in one of the following formats:
+         *
+         * <ul>
+         * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+         * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+         * </li>
+         * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+         * </ul>
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_THUMBNAIL_URI = "thumbnail_uri";
+
+        /**
+         * The flag indicating whether this TV program is searchable or not.
+         *
+         * <p>The columns of searchable programs can be read by other applications that have proper
+         * permission. Care must be taken not to open sensitive data.
+         *
+         * <p>A value of 1 indicates that the program is searchable and its columns can be read by
+         * other applications, a value of 0 indicates that the program is hidden and its columns can
+         * be read only by the package that owns the program and the system. If not specified, this
+         * value is set to 1 (searchable) by default.
+         *
+         * <p>Type: INTEGER (boolean)
+         */
+        public static final String COLUMN_SEARCHABLE = "searchable";
+
+        /**
+         * The flag indicating whether recording of this program is prohibited.
+         *
+         * <p>A value of 1 indicates that recording of this program is prohibited and application
+         * will not schedule any recording for this program. A value of 0 indicates that the
+         * recording is not prohibited. If not specified, this value is set to 0 (not prohibited) by
+         * default.
+         *
+         * <p>Type: INTEGER (boolean)
+         */
+        public static final String COLUMN_RECORDING_PROHIBITED = "recording_prohibited";
+
+        /**
+         * Internal data used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: BLOB
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_DATA = "internal_provider_data";
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG1 = "internal_provider_flag1";
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG2 = "internal_provider_flag2";
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG3 = "internal_provider_flag3";
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG4 = "internal_provider_flag4";
+
+        /**
+         * The version number of this row entry used by TV input services.
+         *
+         * <p>This is best used by sync adapters to identify the rows to update. The number can be
+         * defined by individual TV input services. One may assign the same value as
+         * {@code version_number} in ETSI EN 300 468 or ATSC A/65, if the data are coming from a TV
+         * broadcast.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_VERSION_NUMBER = "version_number";
+
+        /**
+         * The internal ID used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_ID = "internal_provider_id";
+
+        /**
+         * The URI for the preview video.
+         *
+         * <p>This is only relevant to {@link Channels#TYPE_PREVIEW}. The data in the column must be
+         * a URL, or a URI in one of the following formats:
+         *
+         * <ul>
+         * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+         * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+         * </li>
+         * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+         * </ul>
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_PREVIEW_VIDEO_URI = "preview_video_uri";
+
+        /**
+         * The last playback position (in milliseconds) of the preview video.
+         *
+         * <p>This is only relevant to {@link Channels#TYPE_PREVIEW}.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_PREVIEW_LAST_PLAYBACK_POSITION =
+                "preview_last_playback_position";
+
+        /**
+         * The duration (in milliseconds) of the preview video.
+         *
+         * <p>This is only relevant to {@link Channels#TYPE_PREVIEW}.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_PREVIEW_DURATION = "preview_duration";
+
+        /**
+         * The intent URI which is launched when the preview video is selected.
+         *
+         * <p>The URI is created using {@link Intent#toUri} with {@link Intent#URI_INTENT_SCHEME}
+         * and converted back to the original intent with {@link Intent#parseUri}. The intent is
+         * launched when the user selects the preview video item.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_PREVIEW_INTENT_URI =
+                "preview_intent_uri";
+
+        /**
+         * The weight of the preview program within the channel.
+         *
+         * <p>The UI may choose to show this item in a different position in the channel row.
+         * A larger weight value means the program is more important than other programs having
+         * smaller weight values. The value is relevant for the preview programs in the same
+         * channel. This is only relevant to {@link Channels#TYPE_PREVIEW}.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_PREVIEW_WEIGHT = "preview_weight";
+
+        /**
+         * The flag indicating whether this program is transient or not.
+         *
+         * <p>A value of 1 indicates that the channel will be automatically removed by the system on
+         * reboot, and a value of 0 indicates that the channel is persistent across reboot. If not
+         * specified, this value is set to 0 (not transient) by default.
+         *
+         * <p>Type: INTEGER (boolean)
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP)
+        public static final String COLUMN_TRANSIENT = "transient";
+
+        private Programs() {}
+
+        /** Canonical genres for TV programs. */
+        public static final class Genres {
+            /** The genre for Family/Kids. */
+            public static final String FAMILY_KIDS = "FAMILY_KIDS";
+
+            /** The genre for Sports. */
+            public static final String SPORTS = "SPORTS";
+
+            /** The genre for Shopping. */
+            public static final String SHOPPING = "SHOPPING";
+
+            /** The genre for Movies. */
+            public static final String MOVIES = "MOVIES";
+
+            /** The genre for Comedy. */
+            public static final String COMEDY = "COMEDY";
+
+            /** The genre for Travel. */
+            public static final String TRAVEL = "TRAVEL";
+
+            /** The genre for Drama. */
+            public static final String DRAMA = "DRAMA";
+
+            /** The genre for Education. */
+            public static final String EDUCATION = "EDUCATION";
+
+            /** The genre for Animal/Wildlife. */
+            public static final String ANIMAL_WILDLIFE = "ANIMAL_WILDLIFE";
+
+            /** The genre for News. */
+            public static final String NEWS = "NEWS";
+
+            /** The genre for Gaming. */
+            public static final String GAMING = "GAMING";
+
+            /** The genre for Arts. */
+            public static final String ARTS = "ARTS";
+
+            /** The genre for Entertainment. */
+            public static final String ENTERTAINMENT = "ENTERTAINMENT";
+
+            /** The genre for Life Style. */
+            public static final String LIFE_STYLE = "LIFE_STYLE";
+
+            /** The genre for Music. */
+            public static final String MUSIC = "MUSIC";
+
+            /** The genre for Premier. */
+            public static final String PREMIER = "PREMIER";
+
+            /** The genre for Tech/Science. */
+            public static final String TECH_SCIENCE = "TECH_SCIENCE";
+
+            private static final HashSet<String> CANONICAL_GENRES = new HashSet<>();
+            static {
+                CANONICAL_GENRES.add(FAMILY_KIDS);
+                CANONICAL_GENRES.add(SPORTS);
+                CANONICAL_GENRES.add(SHOPPING);
+                CANONICAL_GENRES.add(MOVIES);
+                CANONICAL_GENRES.add(COMEDY);
+                CANONICAL_GENRES.add(TRAVEL);
+                CANONICAL_GENRES.add(DRAMA);
+                CANONICAL_GENRES.add(EDUCATION);
+                CANONICAL_GENRES.add(ANIMAL_WILDLIFE);
+                CANONICAL_GENRES.add(NEWS);
+                CANONICAL_GENRES.add(GAMING);
+                CANONICAL_GENRES.add(ARTS);
+                CANONICAL_GENRES.add(ENTERTAINMENT);
+                CANONICAL_GENRES.add(LIFE_STYLE);
+                CANONICAL_GENRES.add(MUSIC);
+                CANONICAL_GENRES.add(PREMIER);
+                CANONICAL_GENRES.add(TECH_SCIENCE);
+            }
+
+            private static final char DOUBLE_QUOTE = '"';
+            private static final char COMMA = ',';
+            private static final String DELIMITER = ",";
+
+            private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+            private Genres() {}
+
+            /**
+             * Encodes genre strings to a text that can be put into the database.
+             *
+             * @param genres Genre strings.
+             * @return an encoded genre string that can be inserted into the
+             *         {@link #COLUMN_BROADCAST_GENRE} or {@link #COLUMN_CANONICAL_GENRE} column.
+             */
+            public static String encode(@NonNull String... genres) {
+                if (genres == null) {
+                    // MNC and before will throw a NPE.
+                    return null;
+                }
+                StringBuilder sb = new StringBuilder();
+                String separator = "";
+                for (String genre : genres) {
+                    sb.append(separator).append(encodeToCsv(genre));
+                    separator = DELIMITER;
+                }
+                return sb.toString();
+            }
+
+            private static String encodeToCsv(String genre) {
+                StringBuilder sb = new StringBuilder();
+                int length = genre.length();
+                for (int i = 0; i < length; ++i) {
+                    char c = genre.charAt(i);
+                    switch (c) {
+                        case DOUBLE_QUOTE:
+                            sb.append(DOUBLE_QUOTE);
+                            break;
+                        case COMMA:
+                            sb.append(DOUBLE_QUOTE);
+                            break;
+                    }
+                    sb.append(c);
+                }
+                return sb.toString();
+            }
+
+            /**
+             * Decodes the genre strings from the text stored in the database.
+             *
+             * @param genres The encoded genre string retrieved from the
+             *            {@link #COLUMN_BROADCAST_GENRE} or {@link #COLUMN_CANONICAL_GENRE} column.
+             * @return genre strings.
+             */
+            public static String[] decode(@NonNull String genres) {
+                if (TextUtils.isEmpty(genres)) {
+                    // MNC and before will throw a NPE for {@code null} genres.
+                    return EMPTY_STRING_ARRAY;
+                }
+                if (genres.indexOf(COMMA) == -1 && genres.indexOf(DOUBLE_QUOTE) == -1) {
+                    return new String[] {genres.trim()};
+                }
+                StringBuilder sb = new StringBuilder();
+                List<String> results = new ArrayList<>();
+                int length = genres.length();
+                boolean escape = false;
+                for (int i = 0; i < length; ++i) {
+                    char c = genres.charAt(i);
+                    switch (c) {
+                        case DOUBLE_QUOTE:
+                            if (!escape) {
+                                escape = true;
+                                continue;
+                            }
+                            break;
+                        case COMMA:
+                            if (!escape) {
+                                String string = sb.toString().trim();
+                                if (string.length() > 0) {
+                                    results.add(string);
+                                }
+                                sb = new StringBuilder();
+                                continue;
+                            }
+                            break;
+                    }
+                    sb.append(c);
+                    escape = false;
+                }
+                String string = sb.toString().trim();
+                if (string.length() > 0) {
+                    results.add(string);
+                }
+                return results.toArray(new String[results.size()]);
+            }
+
+            /**
+             * Returns whether a given text is a canonical genre defined in {@link Genres}.
+             *
+             * @param genre The name of genre to be checked.
+             * @return {@code true} if the genre is canonical, otherwise {@code false}.
+             */
+            public static boolean isCanonical(String genre) {
+                return CANONICAL_GENRES.contains(genre);
+            }
+        }
+    }
+
+    /**
+     * Column definitions for the recorded TV programs table.
+     *
+     * <p>By default, the query results will be sorted by {@link #COLUMN_START_TIME_UTC_MILLIS} in
+     * ascending order.
+     */
+    public static final class RecordedPrograms implements BaseTvColumns {
+
+        /** The content:// style URI for this table. */
+        public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/"
+                + PATH_RECORDED_PROGRAM);
+
+        /** The MIME type of a directory of recorded TV programs. */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/recorded_program";
+
+        /** The MIME type of a single recorded TV program. */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/recorded_program";
+
+        /**
+         * The ID of the TV input service that is associated with this recorded program.
+         *
+         * <p>Use {@link #buildInputId} to build the ID.
+         *
+         * <p>This is a required field.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_INPUT_ID = "input_id";
+
+        /**
+         * The ID of the TV channel that provided this recorded TV program.
+         *
+         * <p>This is a part of the channel URI and matches to {@link BaseColumns#_ID}.
+         *
+         * <p>This is a required field.
+         *
+         * <p>Type: INTEGER (long)
+         * @see Programs#COLUMN_CHANNEL_ID
+         */
+        public static final String COLUMN_CHANNEL_ID = Programs.COLUMN_CHANNEL_ID;
+
+        /**
+         * The title of this recorded TV program.
+         *
+         * <p>If this recorded program is an episodic TV show, it is recommended that the title is
+         * the series title and its related fields ({@link #COLUMN_SEASON_TITLE} and/or
+         * {@link #COLUMN_SEASON_DISPLAY_NUMBER}, {@link #COLUMN_EPISODE_DISPLAY_NUMBER},
+         * and {@link #COLUMN_EPISODE_TITLE}) are filled in.
+         *
+         * <p>Type: TEXT
+         * @see Programs#COLUMN_TITLE
+         */
+        public static final String COLUMN_TITLE = Programs.COLUMN_TITLE;
+
+        /**
+         * The season display number of this recorded TV program for episodic TV shows.
+         *
+         * <p>This is used to indicate the season number. (e.g. 1, 2 or 3) Note that the value
+         * does not necessarily be numeric. (e.g. 12B)
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_SEASON_DISPLAY_NUMBER =
+                Programs.COLUMN_SEASON_DISPLAY_NUMBER;
+
+        /**
+         * The title of the season for this recorded TV program for episodic TV shows.
+         *
+         * <p>This is an optional field supplied only when the season has a special title
+         * (e.g. The Final Season). If provided, the applications should display it instead of
+         * {@link #COLUMN_SEASON_DISPLAY_NUMBER} without alterations.
+         * (e.g. for "The Final Season", displayed string should be "The Final Season", not
+         * "Season The Final Season"). When displaying multiple programs, the order should be based
+         * on {@link #COLUMN_SEASON_DISPLAY_NUMBER}, even when {@link #COLUMN_SEASON_TITLE} exists.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_SEASON_TITLE = Programs.COLUMN_SEASON_TITLE;
+
+        /**
+         * The episode display number of this recorded TV program for episodic TV shows.
+         *
+         * <p>This is used to indicate the episode number. (e.g. 1, 2 or 3) Note that the value
+         * does not necessarily be numeric. (e.g. 12B)
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         */
+        public static final String COLUMN_EPISODE_DISPLAY_NUMBER =
+                Programs.COLUMN_EPISODE_DISPLAY_NUMBER;
+
+        /**
+         * The episode title of this recorded TV program for episodic TV shows.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         * @see Programs#COLUMN_EPISODE_TITLE
+         */
+        public static final String COLUMN_EPISODE_TITLE = Programs.COLUMN_EPISODE_TITLE;
+
+        /**
+         * The start time of the original TV program, in milliseconds since the epoch.
+         *
+         * <p>Type: INTEGER (long)
+         * @see Programs#COLUMN_START_TIME_UTC_MILLIS
+         */
+        public static final String COLUMN_START_TIME_UTC_MILLIS =
+                Programs.COLUMN_START_TIME_UTC_MILLIS;
+
+        /**
+         * The end time of the original TV program, in milliseconds since the epoch.
+         *
+         * <p>Type: INTEGER (long)
+         * @see Programs#COLUMN_END_TIME_UTC_MILLIS
+         */
+        public static final String COLUMN_END_TIME_UTC_MILLIS = Programs.COLUMN_END_TIME_UTC_MILLIS;
+
+        /**
+         * The comma-separated genre string of this recorded TV program.
+         *
+         * <p>Use the same language appeared in the underlying broadcast standard, if applicable.
+         * (For example, one can refer to the genre strings used in Genre Descriptor of ATSC A/65 or
+         * Content Descriptor of ETSI EN 300 468, if appropriate.) Otherwise, leave empty. Use
+         * {@link Programs.Genres#encode Genres.encode()} to create a text that can be stored in
+         * this column. Use {@link Programs.Genres#decode Genres.decode()} to get the broadcast
+         * genre strings from the text stored in the column.
+         *
+         * <p>Type: TEXT
+         * @see Programs#COLUMN_BROADCAST_GENRE
+         */
+        public static final String COLUMN_BROADCAST_GENRE = Programs.COLUMN_BROADCAST_GENRE;
+
+        /**
+         * The comma-separated canonical genre string of this recorded TV program.
+         *
+         * <p>Canonical genres are defined in {@link Programs.Genres}. Use
+         * {@link Programs.Genres#encode Genres.encode()} to create a text that can be stored in
+         * this column. Use {@link Programs.Genres#decode Genres.decode()} to get the canonical
+         * genre strings from the text stored in the column.
+         *
+         * <p>Type: TEXT
+         * @see Programs#COLUMN_CANONICAL_GENRE
+         * @see Programs.Genres
+         */
+        public static final String COLUMN_CANONICAL_GENRE = Programs.COLUMN_CANONICAL_GENRE;
+
+        /**
+         * The short description of this recorded TV program that is displayed to the user by
+         * default.
+         *
+         * <p>It is recommended to limit the length of the descriptions to 256 characters.
+         *
+         * <p>Type: TEXT
+         * @see Programs#COLUMN_SHORT_DESCRIPTION
+         */
+        public static final String COLUMN_SHORT_DESCRIPTION = Programs.COLUMN_SHORT_DESCRIPTION;
+
+        /**
+         * The detailed, lengthy description of this recorded TV program that is displayed only when
+         * the user wants to see more information.
+         *
+         * <p>TV input services should leave this field empty if they have no additional details
+         * beyond {@link #COLUMN_SHORT_DESCRIPTION}.
+         *
+         * <p>Type: TEXT
+         * @see Programs#COLUMN_LONG_DESCRIPTION
+         */
+        public static final String COLUMN_LONG_DESCRIPTION = Programs.COLUMN_LONG_DESCRIPTION;
+
+        /**
+         * The width of the video for this recorded TV program, in the unit of pixels.
+         *
+         * <p>Together with {@link #COLUMN_VIDEO_HEIGHT} this is used to determine the video
+         * resolution of the current recorded TV program. Can be empty if it is not known or the
+         * recorded program does not convey any video.
+         *
+         * <p>Type: INTEGER
+         * @see Programs#COLUMN_VIDEO_WIDTH
+         */
+        public static final String COLUMN_VIDEO_WIDTH = Programs.COLUMN_VIDEO_WIDTH;
+
+        /**
+         * The height of the video for this recorded TV program, in the unit of pixels.
+         *
+         * <p>Together with {@link #COLUMN_VIDEO_WIDTH} this is used to determine the video
+         * resolution of the current recorded TV program. Can be empty if it is not known or the
+         * recorded program does not convey any video.
+         *
+         * <p>Type: INTEGER
+         * @see Programs#COLUMN_VIDEO_HEIGHT
+         */
+        public static final String COLUMN_VIDEO_HEIGHT = Programs.COLUMN_VIDEO_HEIGHT;
+
+        /**
+         * The comma-separated audio languages of this recorded TV program.
+         *
+         * <p>This is used to describe available audio languages included in the recorded program.
+         * Use either ISO 639-1 or 639-2/T codes.
+         *
+         * <p>Type: TEXT
+         * @see Programs#COLUMN_AUDIO_LANGUAGE
+         */
+        public static final String COLUMN_AUDIO_LANGUAGE = Programs.COLUMN_AUDIO_LANGUAGE;
+
+        /**
+         * The comma-separated content ratings of this recorded TV program.
+         *
+         * <p>This is used to describe the content rating(s) of this recorded program. Each
+         * comma-separated content rating sub-string should be generated by calling
+         * {@link TvContentRating#flattenToString}. Note that in most cases the recorded program
+         * content is rated by a single rating system, thus resulting in a corresponding single
+         * sub-string that does not require comma separation and multiple sub-strings appear only
+         * when the recorded program content is rated by two or more content rating systems. If any
+         * of those ratings is specified as "blocked rating" in the user's parental control
+         * settings, the TV input service should block the current content and wait for the signal
+         * that it is okay to unblock.
+         *
+         * <p>Type: TEXT
+         * @see Programs#COLUMN_CONTENT_RATING
+         */
+        public static final String COLUMN_CONTENT_RATING = Programs.COLUMN_CONTENT_RATING;
+
+        /**
+         * The URI for the poster art of this recorded TV program.
+         *
+         * <p>The data in the column must be a URL, or a URI in one of the following formats:
+         *
+         * <ul>
+         * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+         * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+         * </li>
+         * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+         * </ul>
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         * @see Programs#COLUMN_POSTER_ART_URI
+         */
+        public static final String COLUMN_POSTER_ART_URI = Programs.COLUMN_POSTER_ART_URI;
+
+        /**
+         * The URI for the thumbnail of this recorded TV program.
+         *
+         * <p>The system can generate a thumbnail from the poster art if this column is not
+         * specified. Thus it is not necessary for TV input services to include a thumbnail if it is
+         * just a scaled image of the poster art.
+         *
+         * <p>The data in the column must be a URL, or a URI in one of the following formats:
+         *
+         * <ul>
+         * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+         * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+         * </li>
+         * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+         * </ul>
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: TEXT
+         * @see Programs#COLUMN_THUMBNAIL_URI
+         */
+        public static final String COLUMN_THUMBNAIL_URI = Programs.COLUMN_THUMBNAIL_URI;
+
+        /**
+         * The flag indicating whether this recorded TV program is searchable or not.
+         *
+         * <p>The columns of searchable recorded programs can be read by other applications that
+         * have proper permission. Care must be taken not to open sensitive data.
+         *
+         * <p>A value of 1 indicates that the recorded program is searchable and its columns can be
+         * read by other applications, a value of 0 indicates that the recorded program is hidden
+         * and its columns can be read only by the package that owns the recorded program and the
+         * system. If not specified, this value is set to 1 (searchable) by default.
+         *
+         * <p>Type: INTEGER (boolean)
+         * @see Programs#COLUMN_SEARCHABLE
+         */
+        public static final String COLUMN_SEARCHABLE = Programs.COLUMN_SEARCHABLE;
+
+        /**
+         * The URI of the recording data for this recorded program.
+         *
+         * <p>Together with {@link #COLUMN_RECORDING_DATA_BYTES}, applications can use this
+         * information to manage recording storage. The URI should indicate a file or directory with
+         * the scheme {@link android.content.ContentResolver#SCHEME_FILE}.
+         *
+         * <p>Type: TEXT
+         * @see #COLUMN_RECORDING_DATA_BYTES
+         */
+        public static final String COLUMN_RECORDING_DATA_URI = "recording_data_uri";
+
+        /**
+         * The data size (in bytes) for this recorded program.
+         *
+         * <p>Together with {@link #COLUMN_RECORDING_DATA_URI}, applications can use this
+         * information to manage recording storage.
+         *
+         * <p>Type: INTEGER (long)
+         * @see #COLUMN_RECORDING_DATA_URI
+         */
+        public static final String COLUMN_RECORDING_DATA_BYTES = "recording_data_bytes";
+
+        /**
+         * The duration (in milliseconds) of this recorded program.
+         *
+         * <p>The actual duration of the recorded program can differ from the one calculated by
+         * {@link #COLUMN_END_TIME_UTC_MILLIS} - {@link #COLUMN_START_TIME_UTC_MILLIS} as program
+         * recording can be interrupted in the middle for some reason, resulting in a partially
+         * recorded program, which is still playable.
+         *
+         * <p>Type: INTEGER
+         */
+        public static final String COLUMN_RECORDING_DURATION_MILLIS = "recording_duration_millis";
+
+        /**
+         * The expiration time for this recorded program, in milliseconds since the epoch.
+         *
+         * <p>Recorded TV programs do not expire by default unless explicitly requested by the user
+         * or the user allows applications to delete them in order to free up disk space for future
+         * recording. However, some TV content can have expiration date set by the content provider
+         * when recorded. This field is used to indicate such a restriction.
+         *
+         * <p>Can be empty.
+         *
+         * <p>Type: INTEGER (long)
+         */
+        public static final String COLUMN_RECORDING_EXPIRE_TIME_UTC_MILLIS =
+                "recording_expire_time_utc_millis";
+
+
+        /**
+         * Internal data used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: BLOB
+         * @see Programs#COLUMN_INTERNAL_PROVIDER_DATA
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_DATA =
+                Programs.COLUMN_INTERNAL_PROVIDER_DATA;
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG1
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG1 =
+                Programs.COLUMN_INTERNAL_PROVIDER_FLAG1;
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG2
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG2 =
+                Programs.COLUMN_INTERNAL_PROVIDER_FLAG2;
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG3
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG3 =
+                Programs.COLUMN_INTERNAL_PROVIDER_FLAG3;
+
+        /**
+         * Internal integer flag used by individual TV input services.
+         *
+         * <p>This is internal to the provider that inserted it, and should not be decoded by other
+         * apps.
+         *
+         * <p>Type: INTEGER
+         * @see Programs#COLUMN_INTERNAL_PROVIDER_FLAG4
+         */
+        public static final String COLUMN_INTERNAL_PROVIDER_FLAG4 =
+                Programs.COLUMN_INTERNAL_PROVIDER_FLAG4;
+
+        /**
+         * The version number of this row entry used by TV input services.
+         *
+         * <p>This is best used by sync adapters to identify the rows to update. The number can be
+         * defined by individual TV input services. One may assign the same value as
+         * {@code version_number} in ETSI EN 300 468 or ATSC A/65, if the data are coming from a TV
+         * broadcast.
+         *
+         * <p>Type: INTEGER
+         * @see Programs#COLUMN_VERSION_NUMBER
+         */
+        public static final String COLUMN_VERSION_NUMBER = Programs.COLUMN_VERSION_NUMBER;
+
+        private RecordedPrograms() {}
+    }
+}
diff --git a/tv-provider/src/android/support/media/tv/TvContractUtils.java b/tv-provider/src/android/support/media/tv/TvContractUtils.java
new file mode 100644
index 0000000..2638e34
--- /dev/null
+++ b/tv-provider/src/android/support/media/tv/TvContractUtils.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.tv;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.media.tv.TvContentRating;
+import android.support.annotation.RestrictTo;
+import android.text.TextUtils;
+
+/**
+ * Static helper methods for working with {@link android.media.tv.TvContract}.
+ * @hide
+ */
+@RestrictTo(LIBRARY_GROUP)
+public class TvContractUtils {
+
+    private static final String TAG = "TvContractUtils";
+    private static final boolean DEBUG = false;
+    private static final String DELIMITER = ",";
+
+    /**
+     * Parses a string of comma-separated ratings into an array of {@link TvContentRating}.
+     *
+     * @param commaSeparatedRatings String containing various ratings, separated by commas.
+     * @return An array of TvContentRatings.
+     */
+    public static TvContentRating[] stringToContentRatings(String commaSeparatedRatings) {
+        if (TextUtils.isEmpty(commaSeparatedRatings)) {
+            return null;
+        }
+        String[] ratings = commaSeparatedRatings.split("\\s*,\\s*");
+        TvContentRating[] contentRatings = new TvContentRating[ratings.length];
+        for (int i = 0; i < contentRatings.length; ++i) {
+            contentRatings[i] = TvContentRating.unflattenFromString(ratings[i]);
+        }
+        return contentRatings;
+    }
+
+    /**
+     * Flattens an array of {@link TvContentRating} into a String to be inserted into a database.
+     *
+     * @param contentRatings An array of TvContentRatings.
+     * @return A comma-separated String of ratings.
+     */
+    public static String contentRatingsToString(TvContentRating[] contentRatings) {
+        if (contentRatings == null || contentRatings.length == 0) {
+            return null;
+        }
+        StringBuilder ratings = new StringBuilder(contentRatings[0].flattenToString());
+        for (int i = 1; i < contentRatings.length; ++i) {
+            ratings.append(DELIMITER);
+            ratings.append(contentRatings[i].flattenToString());
+        }
+        return ratings.toString();
+    }
+
+    /**
+     * Parses a string of comma-separated audio languages into an array of audio language strings.
+     *
+     * @param commaSeparatedString String containing audio languages, separated by commas.
+     * @return An array of audio language.
+     */
+    public static String[] stringToAudioLanguages(String commaSeparatedString) {
+        if (TextUtils.isEmpty(commaSeparatedString)) {
+            return null;
+        }
+        return commaSeparatedString.split("\\s*,\\s*");
+    }
+
+    /**
+     * Concatenate an array of audio languages into a String to be inserted into a database.
+     *
+     * @param audioLanguages An array of audio languages.
+     * @return A comma-separated String of audio languages.
+     */
+    public static String audioLanguagesToString(String[] audioLanguages) {
+        if (audioLanguages == null || audioLanguages.length == 0) {
+            return null;
+        }
+        StringBuilder ratings = new StringBuilder(audioLanguages[0]);
+        for (int i = 1; i < audioLanguages.length; ++i) {
+            ratings.append(DELIMITER);
+            ratings.append(audioLanguages[i]);
+        }
+        return ratings.toString();
+    }
+
+    private TvContractUtils() {
+    }
+}
diff --git a/tv-provider/tests/AndroidManifest.xml b/tv-provider/tests/AndroidManifest.xml
new file mode 100644
index 0000000..58257a7
--- /dev/null
+++ b/tv-provider/tests/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+   Copyright (C) 2016 The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="android.support.media.tv.test">
+
+    <uses-sdk
+            android:minSdkVersion="21"
+            android:targetSdkVersion="24"
+            tools:overrideLibrary="android.support.test, android.app, android.support.test.rule,
+                      android.support.test.espresso, android.support.test.espresso.idling"/>
+
+    <application>
+        <uses-library android:name="android.test.runner"/>
+    </application>
+
+    <instrumentation
+            android:name="android.test.InstrumentationTestRunner"
+            android:targetPackage="android.support.media.tv.test"/>
+
+</manifest>
diff --git a/tv-provider/tests/NO_DOCS b/tv-provider/tests/NO_DOCS
new file mode 100644
index 0000000..092a39c
--- /dev/null
+++ b/tv-provider/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
diff --git a/tv-provider/tests/src/android/support/media/tv/ChannelTest.java b/tv-provider/tests/src/android/support/media/tv/ChannelTest.java
new file mode 100644
index 0000000..9318216
--- /dev/null
+++ b/tv-provider/tests/src/android/support/media/tv/ChannelTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.tv;
+
+import android.content.ContentValues;
+import android.content.Intent;
+import android.database.MatrixCursor;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+/**
+ * Tests that channels can be created using the Builder pattern and correctly obtain
+ * values from them
+ */
+@RequiresApi(api = Build.VERSION_CODES.M)
+public class ChannelTest extends TestCase {
+    private static final String KEY_SPLASHSCREEN = "splashscreen";
+    private static final String KEY_PREMIUM_CHANNEL = "premium";
+    private static final String SPLASHSCREEN_URL = "http://example.com/splashscreen.jpg";
+
+    @Test
+    public void testEmptyChannel() {
+        Channel emptyChannel = new Channel.Builder()
+                .build();
+        ContentValues contentValues = emptyChannel.toContentValues();
+        compareChannel(emptyChannel, Channel.fromCursor(getChannelCursor(contentValues)));
+    }
+
+    @Test
+    public void testSampleChannel() {
+        // Tests cloning and database I/O of a channel with some defined and some undefined
+        // values.
+        Channel sampleChannel = new Channel.Builder()
+                .setDisplayName("Google")
+                .setDisplayNumber("3")
+                .setDescription("This is a sample channel")
+                .setOriginalNetworkId(1)
+                .setAppLinkIntentUri(new Intent().toUri(Intent.URI_INTENT_SCHEME))
+                .setOriginalNetworkId(0)
+                .build();
+        ContentValues contentValues = sampleChannel.toContentValues();
+        compareChannel(sampleChannel, Channel.fromCursor(getChannelCursor(contentValues)));
+
+        Channel clonedSampleChannel = new Channel.Builder(sampleChannel).build();
+        compareChannel(sampleChannel, clonedSampleChannel);
+    }
+
+    @Test
+    public void testFullyPopulatedChannel() {
+        Channel fullyPopulatedChannel = new Channel.Builder()
+                .setAppLinkColor(0x00FF0000)
+                .setAppLinkIconUri("http://example.com/icon.png")
+                .setAppLinkIntent(new Intent())
+                .setAppLinkPosterArtUri("http://example.com/poster.png")
+                .setAppLinkText("Open an intent")
+                .setDescription("Channel description")
+                .setDisplayName("Display Name")
+                .setDisplayNumber("100")
+                .setInputId("TestInputService")
+                .setNetworkAffiliation("Network Affiliation")
+                .setOriginalNetworkId(2)
+                .setPackageName("com.example.android.sampletvinput")
+                .setSearchable(false)
+                .setServiceId(3)
+                .setTransportStreamId(4)
+                .setType(TvContractCompat.Channels.TYPE_PREVIEW)
+                .setServiceType(TvContractCompat.Channels.SERVICE_TYPE_AUDIO_VIDEO)
+                .setVideoFormat(TvContractCompat.Channels.VIDEO_FORMAT_240P)
+                .setInternalProviderFlag1(0x4)
+                .setInternalProviderFlag2(0x3)
+                .setInternalProviderFlag3(0x2)
+                .setInternalProviderFlag4(0x1)
+                .build();
+        ContentValues contentValues = fullyPopulatedChannel.toContentValues();
+        compareChannel(fullyPopulatedChannel, Channel.fromCursor(getChannelCursor(contentValues)));
+
+        Channel clonedFullyPopulatedChannel = new Channel.Builder(fullyPopulatedChannel).build();
+        compareChannel(fullyPopulatedChannel, clonedFullyPopulatedChannel);
+    }
+
+    private static void compareChannel(Channel channelA, Channel channelB) {
+        assertEquals(channelA.getAppLinkColor(), channelB.getAppLinkColor());
+        assertEquals(channelA.getAppLinkIconUri(), channelB.getAppLinkIconUri());
+        assertEquals(channelA.getAppLinkIntentUri(), channelB.getAppLinkIntentUri());
+        assertEquals(channelA.getAppLinkPosterArtUri(), channelB.getAppLinkPosterArtUri());
+        assertEquals(channelA.getAppLinkText(), channelB.getAppLinkText());
+        assertEquals(channelA.isSearchable(), channelB.isSearchable());
+        assertEquals(channelA.getDescription(), channelB.getDescription());
+        assertEquals(channelA.getDisplayName(), channelB.getDisplayName());
+        assertEquals(channelA.getDisplayNumber(), channelB.getDisplayNumber());
+        assertEquals(channelA.getId(), channelB.getId());
+        assertEquals(channelA.getInputId(), channelB.getInputId());
+        assertEquals(channelA.getNetworkAffiliation(), channelB.getNetworkAffiliation());
+        assertEquals(channelA.getOriginalNetworkId(), channelB.getOriginalNetworkId());
+        assertEquals(channelA.getPackageName(), channelB.getPackageName());
+        assertEquals(channelA.getServiceId(), channelB.getServiceId());
+        assertEquals(channelA.getServiceType(), channelB.getServiceType());
+        assertEquals(channelA.getTransportStreamId(), channelB.getTransportStreamId());
+        assertEquals(channelA.getType(), channelB.getType());
+        assertEquals(channelA.getVideoFormat(), channelB.getVideoFormat());
+        assertEquals(channelA.toContentValues(), channelB.toContentValues());
+        assertEquals(channelA.toString(), channelB.toString());
+    }
+
+    private static MatrixCursor getChannelCursor(ContentValues contentValues) {
+        String[] rows = Channel.PROJECTION;
+        MatrixCursor cursor = new MatrixCursor(rows);
+        MatrixCursor.RowBuilder builder = cursor.newRow();
+        for (String row: rows) {
+            if (row != null) {
+                builder.add(row, contentValues.get(row));
+            }
+        }
+        cursor.moveToFirst();
+        return cursor;
+    }
+}
diff --git a/tv-provider/tests/src/android/support/media/tv/ProgramTest.java b/tv-provider/tests/src/android/support/media/tv/ProgramTest.java
new file mode 100644
index 0000000..89942ec
--- /dev/null
+++ b/tv-provider/tests/src/android/support/media/tv/ProgramTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.tv;
+
+import android.content.ContentValues;
+import android.database.MatrixCursor;
+import android.media.tv.TvContentRating;
+import android.os.Build;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Tests that programs can be created using the Builder pattern and correctly obtain
+ * values from them.
+ */
+public class ProgramTest extends TestCase {
+    @Test
+    public void testEmptyProgram() {
+        Program emptyProgram = new Program.Builder()
+                .build();
+        ContentValues contentValues = emptyProgram.toContentValues();
+        compareProgram(emptyProgram,
+                Program.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)));
+    }
+
+    @Test
+    public void testSampleProgram() {
+        Program sampleProgram = new Program.Builder()
+                .setTitle("Program Title")
+                .setDescription("This is a sample program")
+                .setChannelId(3)
+                .setEpisodeNumber(5)
+                .setSeasonNumber("The Final Season", 7)
+                .setThumbnailUri("http://www.example.com/programs/poster.png")
+                .setStartTimeUtcMillis(0)
+                .setEndTimeUtcMillis(1000)
+                .build();
+        ContentValues contentValues = sampleProgram.toContentValues();
+        compareProgram(sampleProgram,
+                Program.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)));
+
+        Program clonedSampleProgram = new Program.Builder(sampleProgram).build();
+        compareProgram(sampleProgram, clonedSampleProgram);
+    }
+
+    @Test
+    public void testFullyPopulatedProgram() {
+        Program fullyPopulatedProgram = new Program.Builder()
+                .setSearchable(false)
+                .setChannelId(3)
+                .setThumbnailUri("http://example.com/thumbnail.png")
+                .setAudioLanguages(new String [] {"eng", "kor"})
+                .setBroadcastGenres(new String[] {"Music", "Family"})
+                .setCanonicalGenres(new String[] {TvContractCompat.Programs.Genres.MOVIES})
+                .setContentRatings(new TvContentRating[] {
+                        TvContentRating.createRating("com.android.tv", "US_TV", "US_TV_Y7")})
+                .setDescription("This is a sample program")
+                .setEndTimeUtcMillis(1000)
+                .setEpisodeNumber("Pilot", 0)
+                .setEpisodeTitle("Hello World")
+                .setLongDescription("This is a longer description than the previous description")
+                .setPosterArtUri("http://example.com/poster.png")
+                .setRecordingProhibited(false)
+                .setSeasonNumber("The Final Season", 7)
+                .setSeasonTitle("The Final Season")
+                .setStartTimeUtcMillis(0)
+                .setTitle("Google")
+                .setVideoHeight(1080)
+                .setVideoWidth(1920)
+                .setInternalProviderId("ID-4321")
+                .setPreviewVideoUri("http://example.com/preview-video.mpg")
+                .setPreviewLastPlaybackPosition(0)
+                .setPreviewDuration(60 * 1000)
+                .setPreviewIntentUri("app_link_intent")
+                .setPreviewWeight(100)
+                .setInternalProviderFlag1(0x4)
+                .setInternalProviderFlag2(0x3)
+                .setInternalProviderFlag3(0x2)
+                .setInternalProviderFlag4(0x1)
+                .setTransient(false)
+                .build();
+
+        ContentValues contentValues = fullyPopulatedProgram.toContentValues();
+        compareProgram(fullyPopulatedProgram,
+                Program.fromCursor(getProgramCursor(Program.PROJECTION, contentValues)));
+
+        Program clonedFullyPopulatedProgram = new Program.Builder(fullyPopulatedProgram).build();
+        compareProgram(fullyPopulatedProgram, clonedFullyPopulatedProgram);
+    }
+
+    @Test
+    public void testPreviewProgram() {
+        Program previewProgram = new Program.Builder()
+                .setId(10)
+                .setChannelId(3)
+                .setTitle("Recommended Video 1")
+                .setDescription("You should watch this!")
+                .setPosterArtUri("http://example.com/poster.png")
+                .setInternalProviderFlag2(0x0010010084108410L)
+                .setInternalProviderId("ID-4321")
+                .setPreviewVideoUri("http://example.com/preview-video.mpg")
+                .setPreviewLastPlaybackPosition(0)
+                .setPreviewDuration(60 * 1000)
+                .setPreviewIntentUri("app_link_intent")
+                .setPreviewWeight(100)
+                .setTransient(false)
+                .build();
+
+        String[] partialProjection = {
+                TvContractCompat.Programs._ID,
+                TvContractCompat.Programs.COLUMN_CHANNEL_ID,
+                TvContractCompat.Programs.COLUMN_TITLE,
+                TvContractCompat.Programs.COLUMN_SHORT_DESCRIPTION,
+                TvContractCompat.Programs.COLUMN_POSTER_ART_URI,
+                TvContractCompat.Programs.COLUMN_INTERNAL_PROVIDER_FLAG2,
+                TvContractCompat.Programs.COLUMN_INTERNAL_PROVIDER_ID,
+                TvContractCompat.Programs.COLUMN_PREVIEW_VIDEO_URI,
+                TvContractCompat.Programs.COLUMN_PREVIEW_LAST_PLAYBACK_POSITION,
+                TvContractCompat.Programs.COLUMN_PREVIEW_DURATION,
+                TvContractCompat.Programs.COLUMN_PREVIEW_INTENT_URI,
+                TvContractCompat.Programs.COLUMN_PREVIEW_WEIGHT,
+                TvContractCompat.Programs.COLUMN_TRANSIENT,
+        };
+
+        ContentValues contentValues = previewProgram.toContentValues();
+        compareProgram(previewProgram,
+                Program.fromCursor(getProgramCursor(partialProjection, contentValues)));
+
+        Program clonedFullyPopulatedProgram = new Program.Builder(previewProgram).build();
+        compareProgram(previewProgram, clonedFullyPopulatedProgram);
+    }
+
+    private static void compareProgram(Program programA, Program programB) {
+        assertTrue(Arrays.equals(programA.getAudioLanguages(), programB.getAudioLanguages()));
+        assertTrue(Arrays.deepEquals(programA.getBroadcastGenres(), programB.getBroadcastGenres()));
+        assertTrue(Arrays.deepEquals(programA.getCanonicalGenres(), programB.getCanonicalGenres()));
+        assertEquals(programA.getChannelId(), programB.getChannelId());
+        assertTrue(Arrays.deepEquals(programA.getContentRatings(), programB.getContentRatings()));
+        assertEquals(programA.getDescription(), programB.getDescription());
+        assertEquals(programA.getEndTimeUtcMillis(), programB.getEndTimeUtcMillis());
+        assertEquals(programA.getEpisodeNumber(), programB.getEpisodeNumber());
+        assertEquals(programA.getEpisodeTitle(), programB.getEpisodeTitle());
+        assertEquals(programA.getLongDescription(), programB.getLongDescription());
+        assertEquals(programA.getPosterArtUri(), programB.getPosterArtUri());
+        assertEquals(programA.getId(), programB.getId());
+        assertEquals(programA.getSeasonNumber(), programB.getSeasonNumber());
+        assertEquals(programA.getStartTimeUtcMillis(), programB.getStartTimeUtcMillis());
+        assertEquals(programA.getThumbnailUri(), programB.getThumbnailUri());
+        assertEquals(programA.getTitle(), programB.getTitle());
+        assertEquals(programA.getVideoHeight(), programB.getVideoHeight());
+        assertEquals(programA.getVideoWidth(), programB.getVideoWidth());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            assertEquals(programA.isSearchable(), programB.isSearchable());
+            assertEquals(programA.getInternalProviderFlag1(), programB.getInternalProviderFlag1());
+            assertEquals(programA.getInternalProviderFlag2(), programB.getInternalProviderFlag2());
+            assertEquals(programA.getInternalProviderFlag3(), programB.getInternalProviderFlag3());
+            assertEquals(programA.getInternalProviderFlag4(), programB.getInternalProviderFlag4());
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            assertTrue(Objects.equals(programA.getSeasonTitle(), programB.getSeasonTitle()));
+            assertTrue(Objects.equals(programA.isRecordingProhibited(),
+                    programB.isRecordingProhibited()));
+        }
+        assertEquals(programA.toContentValues(), programB.toContentValues());
+        assertEquals(programA.toString(), programB.toString());
+        assertEquals(programA, programB);
+    }
+
+    private static MatrixCursor getProgramCursor(String[] projection, ContentValues contentValues) {
+        MatrixCursor cursor = new MatrixCursor(projection);
+        MatrixCursor.RowBuilder builder = cursor.newRow();
+        for (String row: projection) {
+            if (row != null) {
+                builder.add(row, contentValues.get(row));
+            }
+        }
+        cursor.moveToFirst();
+        return cursor;
+    }
+}