Step 3: Insert Your First Channel
Now it's time to insert your first channel. Review the following diagram and the diagram for TIF architecture in Android development basics.

- Add permissions to the manifest
- Insert channel metadata into Android's TV database
- Insert CDF Station ID
- Insert Gracenote ID
- Insert deep link
- Checkpoint: Display one channel on Fire TV's UI
- Troubleshooting
The TV Input inserts channel and program metadata into the TV Input Framework (TIF) database. Fire TV uses this data to display your service's live content in the Fire TV Live sections. TV Input channel and program metadata must be up-to-date and match the data inside the app. Steps 3 and 4 demonstrate how to insert this data and keep it up-to-date.
Add permissions to the manifest
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" />
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" />
These permissions must be added to AndroidManifest.xml
before your app can interact with the TIF database.
Insert channel metadata into Android's TV database
There are two ways you can insert a basic channel into Android's TV Database. You can insert the channel to a class or an object.
First approach: SetupActivity class
import android.content.ContentValues;
import android.media.tv.TvContract;
import android.util.Log;
import android.net.Uri;
private long insertChannel() {
ContentValues values = new contentValues();
values.put(TvContract.Channels.COLUMN_INPUT_ID, "com.example.android.sampletvinput/.RichTvInputService");
values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "My Test Channel");
values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, "3");
Uri uri = getApplicationContext().getContentResolver().insert(TvContract.Channels.CONTENT_URI, values);
Log.i("SetupActivity", "Channel Inserted! Uri: " + uri);
long channelId = Long.parseLong(uri.getLastPathSegment());
return channelId;
}
import android.app.Activity
import android.content.ContentValues
import android.media.tv.TvContract
import android.net.Uri
import android.util.Log
private fun insertChannel(): Long? {
val values = ContentValues().apply {
put(TvContract.Channels.COLUMN_INPUT_ID, "com.example.android.sampletvinput/.RichTvInputService")
put(TvContract.Channels.COLUMN_DISPLAY_NAME, "My Test Channel")
put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, "3")
}
val uri: Uri? = applicationContext.contentResolver.insert(TvContract.Channels.CONTENT_URI, values)
Log.i("SetupActivity", "Channel Inserted! Uri: $uri")
return uri?.lastPathSegment?.toLongOrNull()
}
Second approach: Channel object provided in the AndroidX library
import androidx.tvprovider.media.tv.Channel;
import android.media.tv.TvContract;
import android.util.Log;
import android.net.Uri;
private long insertChannel() {
Channel testChannel = new Channel.Builder()
.setDisplayName("My Test Channel")
.setDisplayNumber("3")
.setInputId("com.example.android.sampletvinput/.RichTvInputService")
.build();
Uri uri = getApplicationContext().getContentResolver().insert(TvContract.Channels.CONTENT_URI, testChannel.toContentValues());
Log.i("SetupActivity", "Channel Inserted! Uri: " + uri);
long channelId = Long.parseLong(uri.getLastPathSegment());
return channelId;
}
import android.app.Activity
import android.content.Context
import android.media.tv.TvContract
import android.util.Log
import androidx.tvprovider.media.tv.Channel
private fun insertChannel(): Long? {
val testChannel = Channel.Builder()
.setDisplayName("My Test Channel")
.setDisplayNumber("3")
.setInputId("com.example.android.sampletvinput/.RichTvInputService")
.build()
val uri: Uri? =
contentResolver.insert(
TvContract.Channels.CONTENT_URI,
testChannel.toContentValues()
)
Log.i("SetupActivity", "Channel Inserted! Uri: $uri")
return uri?.lastPathSegment?.toLongOrNull()
}
Next, invoke the method in the onCreate()
method in your SetupActivity
(replace the existing code).
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.rich_setup);
insertChannel();
}
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.rich_setup)
insertChannel()
}
Activity | Required? | Input | Notes |
---|---|---|---|
COLUMN_INPUT_ID |
Yes | The full class path to the TvInputService | Example: TvInputService is in the main app package, then the full class path is <app package>/<relative path to your TvInputService>. If TvInputService is in a separate package, then the input ID is <app package>/<full separate package + path to TvInputService>. |
TvContract.Channels.CONTENT_URI |
Yes | This is the URI pointing to the channel table in Android's TV Database. | |
ContentResolver.bulkInsert() or ContentResolver.applyBatch() |
Yes, in production code | One of these ensures all channel insertions happen with one database operation. |
ContentValues
instance, using Android's Channel columns. Here is a reference of all available metadata columns for a channel.Insert CDF Station ID
If you are not using Catalog Data Format (CDF), skip this section.
Amazon's linear catalog ingestion system, the underlying technical integration, lets you upload your linear channel schedules and programming metadata directly to the Amazon Fire TV catalog. If you are interested in integrating with CDF, contact your Amazon representative for more information.
Here is an example of inserting the unique CDF Station ID into a JSON object using Amazon's contract keys to indicate both the type and ID. You can place this inside the insert channels function in SetupActivity
.
/**
* Variable to store the type of external ID, which is used for the matching service metadata. Valid types are
* defined below as constants with prefix "EXTERNAL_ID_TYPE_"
* Null or invalid data will result in failed service match of metadata
*/
private final static String EXTERNAL_ID_TYPE = "externalIdType";
/**
* Variable to store the value of external ID, which is used for the matching service metadata.
* Null or invalid data will result in failed service match of metadata
*/
private final static String EXTERNAL_ID_VALUE = "externalIdValue";
public void fillStationId(ContentValues values) {
try {
String jsonString = new JSONObject()
.put(EXTERNAL_ID_TYPE, "#Actual Id Type#") // replace with unique namespace of your Amazon catalog e.g., "test_cdf2"
.put(EXTERNAL_ID_VALUE, "#Actual Id Value#") // replace with unique CDF Station ID associated with channel e.g., "station-001", "station-child-001", "station-002", "station-child-002", "station-003", "station-child-003"
.toString();
// Add JSON string into the Channel contentValues
values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.getBytes());
} catch (JSONException e) {
Log.e(TAG, "Error when adding data to blob " + e);
}
}
public fun fillStationId(values: ContentValues) {
try {
val jsonString = JSONObject()
.put(
EXTERNAL_ID_TYPE,
"#Actual Id Type#"
) // replace with unique namespace of your Amazon catalog e.g., "test_cdf2"
.put(
EXTERNAL_ID_VALUE,
"#Actual Id Value#"
) // replace with unique CDF Station ID associated with channel e.g., "station-001", "station-child-001", "station-002", "station-child-002", "station-003", "station-child-003"
.toString()
// Add JSON string into the Channel contentValues
values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.toByteArray())
} catch (JSONException e) {
Log.e(TAG, "Error when adding data to blob " + e)
}
}
companion object {
/**
* Variable to store the type of external ID, which is used for the matching service metadata. Valid types are
* defined below as constants with prefix "EXTERNAL_ID_TYPE_"
* Null or invalid data will result in failed service match of metadata
*/
private const val EXTERNAL_ID_TYPE: String = "externalIdType"
/**
* Variable to store the value of external ID, which is used for the matching service metadata.
* Null or invalid data will result in failed service match of metadata
*/
private const val EXTERNAL_ID_VALUE: String = "externalIdValue"
}
Activity | Required? | Notes |
---|---|---|
externalIdType and externalIdValue |
Yes | These field names are part of the contract between developers and Amazon to provide CDF information to Fire TV. Do not change these strings. |
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA |
Yes | This is part of the contract between developers and Amazon to provide deep link and CDF information to Fire TV. |
If you don't have your CDF Station ID setup yet, you can temporarily use the following for development purposes. You can use the sample IDs station-001
, station-002
, or station-003
with the test_cdf2
as provider namespace.
Insert Gracenote ID
If you aren't using Gracenote, skip this section.
Gracenote is a TV catalog provider that integrates with Fire TV to provide channel and program metadata from the cloud. If your content is integrated with Gracenote, you may provide unique IDs which Fire TV uses to gather this metadata. If you're interested in integrating with Gracenote, contact your Amazon representative for more information.
Here is an example of inserting the unique channel Gracenote ID into a JSON object using Amazon's contract keys to indicate both the type and ID. You can place this inside the insert channels function in SetupActivity
.
/**
* Variable to store the type of external ID, which is used for the matching service metadata. Valid types are
* defined below as constants with prefix "EXTERNAL_ID_TYPE_"
* Null or invalid data will result in failed service
* match of metadata
*/
private final static String EXTERNAL_ID_TYPE = "externalIdType";
/**
* Variable to store the value of external ID, which is used for the matching service metadata.
* Null or invalid data will result in failed service match of metadata
*/
private final static String EXTERNAL_ID_VALUE = "externalIdValue";
/**
* Uri for deep link of playback into external player.
*/
private final static String PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri";
// The Id for Gracenote input type
private final static String GRACENOTE_ID = "gracenote_ontv"; // gracenote ontv id
private final static String GRACENOTE_GVD = "gracenote_gvd"; // gracenote gvd id
// Contract for playback deep link uri
// Use Intent.URI_INTENT_SCHEME to create uri from intent and to covert back to original intent
Intent playbackDeepLinkIntent = new Intent(); // Created by your app
String playbackDeepLinkUri = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME);
// Construct BLOB
ContentValues values = new ContentValues(); // store all the channel data
ContentResolver resolver = context.getContentResolver();
values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, "#Actual display name#");
values.put(TvContract.Channels.COLUMN_INPUT_ID, "#Actual input id#");
try {
String jsonString = new JSONObject()
.put(EXTERNAL_ID_TYPE, "#Actual Id Type#") // replace with GRACENOTE_XXX
.put(EXTERNAL_ID_VALUE, "#Actual Id Value#") // replace with gracenote ID value associated with channel
.put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri).toString();
values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.getBytes());
} catch (JSONException e) {
Log.e(TAG, "Error when adding data to blob " + e);
}
Uri uri = resolver.insert(TvContract.Channels.CONTENT_URI, values);
import android.app.Activity
import android.content.ContentValues
import android.content.Intent
import android.media.tv.TvContract
import android.util.Log
import org.json.JSONException
import org.json.JSONObject
/**
* Variable to store the type of external ID, which is used for the matching service metadata. Valid
* types are defined below as constants with prefix "EXTERNAL_ID_TYPE_" Null or invalid data will
* result in failed service match of metadata
*/
private const val EXTERNAL_ID_TYPE = "externalIdType"
/**
* Variable to store the value of external ID, which is used for the matching service metadata. Null
* or invalid data will result in failed service match of metadata
*/
private const val EXTERNAL_ID_VALUE = "externalIdValue"
/**
* Uri for deep link of playback into external player.
*/
private const val PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri"
// The Id for Gracenote input type
private const val GRACENOTE_ID = "gracenote_ontv" // gracenote ontv id
private const val GRACENOTE_GVD = "gracenote_gvd" // gracenote gvd id
class SetupActivity : Activity() {
private fun insertChannel(): Long? {
// Contract for playback deep link uri
// Use Intent.URI_INTENT_SCHEME to create uri from intent and to covert back to original intent
val playbackDeepLinkIntent = Intent() // Created by your app
val playbackDeepLinkUri = playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME)
val jsonString: String? = try {
JSONObject()
.put(EXTERNAL_ID_TYPE, "#Actual Id Type#") // replace with GRACENOTE_XXX
.put(
EXTERNAL_ID_VALUE,
"#Actual Id Value#"
) // replace with gracenote ID value associated with channel
.put(PLAYBACK_DEEP_LINK_URI, playbackDeepLinkUri)
.toString()
} catch (e: JSONException) {
Log.e(TAG, "Error when adding data to blob", e)
null
}
// Construct BLOB
val values = ContentValues().apply { // store all the channel data
put(TvContract.Channels.COLUMN_DISPLAY_NAME, "#Actual display name#")
put(TvContract.Channels.COLUMN_INPUT_ID, "#Actual input id#")
if (jsonString != null) {
put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.toByteArray())
}
}
val uri = contentResolver.insert(TvContract.Channels.CONTENT_URI, values)
Log.i("SetupActivity", "Channel Inserted! Uri: $uri")
return uri?.lastPathSegment?.toLongOrNull()
}
}
private val TAG = "MyTAG"
Activity | Required? | Notes |
---|---|---|
externalIdType and externalIdValue |
Yes | These field names are part of the contract between developers and Amazon to provide Gracenote information to Fire TV. Do not change these strings. |
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA |
Yes | This is part of the contract between developers and Amazon to provide deep link and Gracenote information to Fire TV. |
- If you have another type of Gracenote ID, check to see which kind. If you are uncertain about this, contact your Amazon representative.
-
If you plan to use Gracenote, but don't have your Gracenote ID yet, you can temporarily use the following for development purposes. In the US/UK/DE, you can use these sample IDs: 10171 (Disney Channel), 10240 (HBO), and 12131 (Cartoon Network), with the gracenote_ontv
externalIdType
. For all other marketplaces, you can use sample IDGN9BBXQSECYVNGW
(HBO) with the gracenote_gvdexternalIdType
.Important: If a channel supports deep link and Gracenote ID, both should be inserted into the same JSON object using the above contract.
Insert deep link
Insert the deep link into a JSON object using Amazon's contract key string playbackDeepLinkUri
.
/**
* Uri for deep link of playback into external player.
*/
private final static String AMZ_KEY_PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri";
...
Intent playbackDeepLinkIntent = new Intent();
...
// construct the channel contentValues
ContentValues values = new contentValues();
values.put(Channels.COLUMN_INPUT_ID, inputId);
values.put(Channels.COLUMN_DISPLAY_NAME, channel.name);
...
// construct the deep link Intent
playbackDeepLinkIntent = //deep link intent for provider's channel
...
try {
String jsonString = new JSONObject()
.put(AMZ_KEY_PLAYBACK_DEEP_LINK_URI, playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME))
.toString();
// add jsonString into the channel contentValues
values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, jsonString.getBytes());
} catch (JSONException e) {
Log.i(TAG, "Error when adding data to blob " + e);
}
Uri uri = context.getContentResolver().insert(TvContract.Channels.CONTENT_URI, values);
import android.app.Activity
import android.content.ContentValues
import android.content.Intent
import android.media.tv.TvContract
import android.util.Log
import org.json.JSONException
import org.json.JSONObject
/**
* Uri for deep link of playback into external player.
*/
private const val AMZ_KEY_PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri"
class SetupActivity : Activity() {
private fun insertChannel(): Long? {
val playbackDeepLinkIntent = createPlaybackDeepLinkIntent() //deep link intent for provider's channel
// construct the channel contentValues
val values = ContentValues().apply {
put(
TvContract.Channels.COLUMN_INPUT_ID,
"com.example.android.sampletvinput/.RichTvInputService"
)
put(TvContract.Channels.COLUMN_DISPLAY_NAME, "My Test Channel")
}
try {
val jsonString = JSONObject()
.put(
AMZ_KEY_PLAYBACK_DEEP_LINK_URI,
playbackDeepLinkIntent.toUri(Intent.URI_INTENT_SCHEME)
)
.toString()
// add jsonString into the channel contentValues
values.put(
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA,
jsonString.toByteArray()
)
} catch (e: JSONException) {
Log.i("SetupActivity", "Error when adding data to blob $e")
}
val uri = contentResolver.insert(TvContract.Channels.CONTENT_URI, values)
Log.i("SetupActivity", "Channel Inserted! Uri: $uri")
return uri?.lastPathSegment?.toLongOrNull()
}
private fun createPlaybackDeepLinkIntent(): Intent = TODO()
}
Activity | Required? | Notes |
---|---|---|
playbackDeepLinkUri |
Yes | This is part of the contract between developers and Amazon to provide a channel deep link to Fire TV. Do not change this string. |
TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA |
Yes | This is part of the contract between developers and Amazon to provide deep link and Gracenote information to Fire TV. |
Checkpoint: Display one channel on Fire TV's UI
Use the following steps to verify your setup.
- Build and install your APK on a Fire TV.
- Navigate to Settings > Live TV > Sync Sources and select the source.
- Navigate to Home > On Now row. The inserted channel should appear as one of the cards (a box with content, sometimes called a tile). For non-Gracenote or non-CDF integration, you will see a gray tile with a channel name. If there are many channels on your device from other sources, your channel might not appear because there is a limit.
- Navigate to Live TV > Channel Guide, open the Options menu (3 lines) > Filter Channels > your input name. The inserted channel should appear on the screen as a row.
- Navigate to Settings > Live TV > Manage Channels. Your input name (from the job service XML file) should appear under the list and have the inserted channel assigned to it.
- If using deep link, click the channel card in the On Now row. The app should launch and display the expected channel.
If you integrated with Gracenote or CDF, the channel shows the complete program metadata in the On Now row and in the Channel Guide.
Troubleshooting
This section contains steps to troubleshoot issues you might encounter.
- The channel is not showing up in the On Now row or Channel Guide.
-
- Refer to the Checkpoint to verify it's added to the allow list.
- Verify that the input ID of that channel equals the full class path to TvInputService.
- Verify that the debug APKs and the production APKs have the same package names.
- Verify that the channel is correctly inserted into TIF.
- Create a hard-code query for channel information right after the insert to make sure the channel is in the database.
- Verify that Amazon picks up the channel correctly.
-
Before inserting the channel, view ADB logs:
For Mac / Linux
adb logcat | grep StationSync
For Windows
adb logcat | findstr StationSync
-
After you insert the channel, you should be able to see logs similar to what you find below. Added means Amazon is recognizing a new channel in Android's TV Database.
-
08-07 15:24:57.101 11882 11941 I StationSync: Started full channel sync 08-07 15:24:57.188 11882 11941 I StationSync: Finished full channel sync, found: 15, added: 1, removed: 0, updated: 0
- The channel is shown as a blank tile in On Now, with no image (only a channel name).
-
- This is expected behavior if a channel is not Gracenote or CDF integrated. If it is Gracenote or CDF integrated, see below.
- The channel has a Gracenote ID or CDF ID, but no metadata shows up in On Now or the Channel Guide.
-
- Make sure you know if your feed is for onTV or GVD or CDF and define it correctly in
TvContractUtils
. Amazon catalog supports onTV for certain marketplaces. If there is a mismatch between what Amazon supports and what type of Gracenote ID or CDF ID you have, please reach out to your Amazon contact. They will likely work with Gracenote to correct the issue, or switch to TIF. - Double check the Gracenote ID or CDF ID value. onTV utilizes numerical values only, while GVD and CDF are alphanumeric.
Note: Here is a resource to query Android's TV Database for existing channels or programs. - Make sure you know if your feed is for onTV or GVD or CDF and define it correctly in
Last updated: May 05, 2025