Developer Console

Step 3: Insert Your First Channel

Now it’s time to insert your first channel. Besides this diagram, review the diagram for TIF architecture under Android Development Basics.

Flow for inserting your channel

The TV Input inserts channel and program metadata into the TV Input Framework (TIF) database. This data will be used 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 will be able to interact with the TIF database.

Insert Channel Metadata into Android’s TV Database

Here is an example of how to insert a bare-bones channel into Android’s TV Database. Add the following code to your SetupActivity class: There are two ways you can insert a bare-bones 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 inputId should be <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.

Insert GracenoteId

If you are not 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 the Fire TV will use to gather this metadata. If you are interested in integrating with Gracenote, reach out to your Amazon contact 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.
 * Null or invalid data will result in default as integrated with Fire TV Native 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. Null or invalid data will result in default
 * as integrated with Fire TV Native 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 deeplink 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, reach out to your Amazon contact.
  • If you plan to use Gracenote, but don't have your Gracenote ID yet, you may 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 ID GN9BBXQSECYVNGW (HBO) with the gracenote_gvd externalIdType.

If you've chosen to use deeplink, insert the deeplink into a JSON object using Amazon's contract key string playbackDeepLinkUri.

/**
 * Uri for deep link of playback into external player.
 * Null or invalid data will result in default as integrated with GLive TV Native 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 deeplink Intent
playbackDeepLinkIntent = //deeplink 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. Null or invalid data will result in default
 * as integrated with GLive TV Native player
 */
private const val AMZ_KEY_PLAYBACK_DEEP_LINK_URI = "playbackDeepLinkUri"

class SetupActivity : Activity() {
    private fun insertChannel(): Long? {
        val playbackDeepLinkIntent = createPlaybackDeepLinkIntent() //deeplink 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 deeplink 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 deeplink and Gracenote information to Fire TV.

Checkpoint - Display One Channel On Fire TV’s UI

  1. Build and install your APK onto Fire TV.
  2. Navigate to Settings > Live TV > Sync Sources and select the source.
  3. Navigate to Home > On Now row. The inserted channel should appear as one of the cards (boxes with content, sometimes called a tile). For non-Gracenote, you will see a gray tile with a channel name. If there are a number of channels on your device from other sources, yours may not show (there is a limit).
  4. 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.
  5. 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.
  6. (If using deeplink) Click the channel card in the On Now row. The app should launch and display the expected channel.
  7. (if Gracenote integrated) The channel will show the full program metadata in the On Now row and in the Channel Guide.

Troubleshooting

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 inputId 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 being correctly inserted into TIF.
    • Create a hard-code query for channel information right after the insert to ensure 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 integrated. If it is Gracenote integrated, see below.

The channel has a Gracenote 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 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 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 value. onTV utilizes numerical values only, while GVD is alphanumeric.

Next Steps

If you are not using Gracenote, go to the next step: Step 4: Insert Programs.

If you are using Gracenote, skip Step 4 and go to the following: Step 5: Playback in Fire TV UI.


Last updated: Aug 12, 2022