Content Integration with Android TV Home Screen Channels (Kotlin)

1. Concepts and setup

In this codelab, you'll learn how to build an app that adds channels and programs to the Android TV home screen using the Kotlin and AndroidX libraries. The home screen has more features than this codelab covers. Read the documentation to learn about all of the features and capabilities of the home screen.

Concepts

The Android TV home screen, or simply the home screen, provides a UI that displays recommended content as a table of channels and programs. Each row is a channel. A channel contains cards for every program available on that channel. Your app can offer any number of channels for the user to add to the home screen. A user usually has to select and approve each channel before it appears on the home screen.

Every app has the option of creating one default channel. The default channel is special because it automatically appears in the home screen; the user does not have to explicitly request it.

aa0471dc91b5f815.png

Overview

This codelab shows how to create, add, and update channels and programs on the home screen. It uses a mock database of collections and movies. For simplicity, the same list of movies is used for all of the subscriptions.

Clone the starter project repo

This codelab uses Android Studio, an IDE for developing Android apps.

If you don't have it installed yet, please download and install it.

You can download the source code from the GitHub repository:

git clone https://github.com/googlecodelabs/tv-recommendations-kotlin.git

Or you can download it as a zip file.

Open Android Studio and click File > Open from the menu bar or Open an Existing Android Studio Project from the splash-screen and select the recently cloned folder.

c0e57864138c1248.png

Understanding the starter project

bd4f805254260df7.png

There are four steps in the project. In each step, you'll add more and more code to the app, and after you have completed all of the instructions in each section, you can compare the result with the code in the next step.

These are the main components in the app:

  • MainActivity is the entry activity of the project.
  • model/TvMediaBackground is an object for background images while browsing movies.
  • model/TvMediaCollection is an object for movie collections.
  • model/TvMediaMetadata is an object for storing movie info.
  • model/TvMediaDatabase is the database holder and serves as the main access point for the underlying movie data.
  • fragments/NowPlayingFragment plays movies.
  • fragments/MediaBrowserFragment is the media browser fragment.
  • workers/TvMediaSynchronizer is a data synchronizer class containing code for fetching feeds, constructing objects, and updating channels.
  • utils/TvLauncherUtils is a helper class to manage channels and preview programs using the AndroidX library and TV providers.

Run the starter project

Try running the project. If you have issues, see the documentation on how to get started.

  1. Connect your Android TV or start the emulator.
  1. Select the step_1 configuration, select your Android device and press the run button in the menu bar. ba443677e48e0f00.png
  2. You should see a simple TV app outline with three collections of videos.

364574330c4e90a5.png

What you've learned

In this introduction, you've learned about:

  • The TV home screen and its channels
  • The project code structure and major classes in this codelab

What's next?

Adding channels to the home screen

2. Adding Channels to the Home Screen

Start by adding channels to the home screen. Once you have channels, you can insert programs into the channels. Users are able to discover your channels in the channel configuration panel and select which ones to display in the home UI. This codelab creates channels for each of the media collections:

  • Historical Feature Films
  • 1910's Feature Films
  • Charlie Chaplin Collection

The following section explains how to load data and use it for channels.

The synchronize() method in TvMediaSynchronizer does the following:

  1. Fetches the media feed, which includes background images, media collections, and video metadata. This information is defined in assets/media-feed.json
  2. Updates the TvMediaDatabase instance, which stores background images, media collections, and video data, to its respective objects
  3. Uses TvLauncherUtils to create or update channels and programs

Don't worry about data loading in this codelab. The goal of this codelab is to understand how to use the AndroidX library to create channels. To do this, you will add code to a few methods in the TvLauncherUtils class.

Creating a channel

After fetching media data and saving it to a local database, the project code converts a media Collection into a channel. The code creates and updates channels in the upsertChannel() method of the TvLauncherUtils class.

  1. Create an instance of PreviewChannel.Builder(). To avoid duplicate channels, this codelab checks for the existence of a channel and only updates it if it exists. Each video collection has an associated id, and you can use it as the internalProviderId of a channel. To identify an existing channel, compare its internalProviderId to the collection id. Copy and paste the following code into upsertChannel()at the code comment // TODO: Step 1 create or find an existing channel.
val channelBuilder = if (existingChannel == null) {
   PreviewChannel.Builder()
} else {
   PreviewChannel.Builder(existingChannel)
}
  1. Set attributes in the Builder of a channel (for example, the channel name and logo/icon). The display name appears on the home screen right below the channel's icon. Android TV uses the appLinkIntentUri to navigate users when they click a channel icon. This codelab uses this Uri to lead users to the corresponding collection in the app. Copy and paste the following code at the code comment // TODO: Step 2 add collection metadata and build channel object.
val updatedChannel = channelBuilder
       .setInternalProviderId(collection.id)
       .setLogo(channelLogoUri)
       .setAppLinkIntentUri(appUri)
       .setDisplayName(collection.title)
       .setDescription(collection.description)
       .build()
  1. Call functions in the PreviewChannelHelper class to insert the channel into the TV provider or update it. The call to publishChannel() inserts the channel's content values into the TV provider. updatePreviewChannel updates the existing channels. Insert the following code at the code comment // TODO: Step 3.1 update an existing channel.
PreviewChannelHelper(context)
       .updatePreviewChannel(existingChannel.id, updatedChannel)
Log.d(TAG, "Updated channel ${existingChannel.id}")

Insert the code below to create a new channel at the code comment // TODO: Step 3.2 publish a channel.

val channelId = PreviewChannelHelper(context).publishChannel(updatedChannel)
Log.d(TAG, "Published channel $channelId")
channelId
  1. Review the upsertChannel() method to see how channels are created or updated.

Make the default channel visible

When you add channels to the TV Provider, they are invisible. A channel does not show up on the home screen until the user asks for it. The user usually has to select and approve each channel before it appears on the home screen. Every app has the option of creating one default channel. The default channel is special because it automatically appears on the home screen; the user does not have to explicitly approve it.

Add the following code to the upsertChannel() method (at TODO: step 4 make default channel visible):

if(allChannels.none { it.isBrowsable }) {
   TvContractCompat.requestChannelBrowsable(context, channelId)
}

If you call requestChannelBrowsable() for non-default channels, a dialog will show up asking for the user's consent.

Scheduling channel updates

After adding the channel creation/update code, developers need to invoke the synchronize() method to create the channel or update it.

The best time to create your app's channels is right after the user has installed it. You can create a broadcast receiver to listen to the android.media.tv.action.INITIALIZE_PROGRAMS broadcast message. This broadcast will be sent after the user has installed the TV app, and developers can do some program initialization there.

Check out the AndroidManifest.xml file in the sample code and find the broadcast receiver section. Try to locate the correct class name for the broadcast receiver (it will be covered next).

<action
   android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />

Open the TvLauncherReceiver class and take a look at the following code block to see how the sample app creates home screen channels.

TvContractCompat.ACTION_INITIALIZE_PROGRAMS -> {
   Log.d(TAG, "Handling INITIALIZE_PROGRAMS broadcast")
   // Synchronizes all program and channel data
   WorkManager.getInstance(context).enqueue(
           OneTimeWorkRequestBuilder<TvMediaSynchronizer>().build())
}

You should update your channels regularly. This codelab creates background tasks using the WorkManager library. In the MainActivity class TvMediaSynchronizer is used to schedule regular channel updates.

// Syncs the home screen channels hourly
// NOTE: It's very important to keep our content fresh in the user's home screen
WorkManager.getInstance(baseContext).enqueue(
       PeriodicWorkRequestBuilder<TvMediaSynchronizer>(1, TimeUnit.HOURS)
               .setInitialDelay(1, TimeUnit.HOURS)
               .setConstraints(Constraints.Builder()
                       .setRequiredNetworkType(NetworkType.CONNECTED)
                       .build())
               .build())

Run the app

Run the app. Go to the home screen. The default (My TV App Default) channel appears, but it does not have any programs. If you are running the code on a real device rather than an emulator, the channel may not appear.

f14e903b0505a281.png

Add more channels

The feed contains three collections. In the TvMediaSynchronizer class, add other channels for these collections (at TODO: step 5 add more channels).

feed.collections.subList(1, feed.collections.size).forEach {
   TvLauncherUtils.upsertChannel(
           context, it, database.metadata().findByCollection(it.id))
}

Run the app again

Verify that all three channels were created. Click the Customize Channels button and then TV Classics. Toggle the hide/show button in the channel panel to hide/show them on the home screen.

faac02714aa36ab6.png

Delete a channel

If the app is no longer maintaining a channel, you can remove it from the home screen.

Search for step 6 and find the removeChannel function. Add the following section into it (at TODO: step 6 remove a channel). To see how this code works, remove the collection titled with "Charlie Chaplin Collection" in media-feed.json (be sure to remove the whole collection). Run the app again, and after a few seconds, you will see that the channel is removed.

// First, get all the channels added to the home screen
val allChannels = PreviewChannelHelper(context).allChannels

// Now find the channel with the matching content ID for our collection
val foundChannel = allChannels.find { it.internalProviderId == collection.id }
if (foundChannel == null) Log.e(TAG, "No channel with ID ${collection.id}")

// Use the found channel's ID to delete it from the content resolver
return foundChannel?.let {
   PreviewChannelHelper(context).deletePreviewChannel(it.id)
   Log.d(TAG, "Channel successfully removed from home screen")

   // Remove all of the channel programs as well
   val channelPrograms =
           TvContractCompat.buildPreviewProgramsUriForChannel(it.id)
   context.contentResolver.delete(channelPrograms, null, null)

   // Return the ID of the channel removed
   it.id
}

After you've finished all instructions above, you can compare the app code with step_2.

What you've learned

  • How to query for channels.
  • How to add or delete channels from the home screen.
  • How to set a logo or title on a channel.
  • How to make a default channel visible.
  • How to schedule a WorkManager to update channels.

What's next?

The next section shows how to add programs to a channel.

3. Adding Programs to Channels

Adding a program to a channel is similar to creating a channel. Use PreviewProgram.Builder instead of PreviewChannel.Builder.

You'll be still working with the upsertChannel() method in the TvLauncherUtils class.

Create a Preview Program

We are going to add code to step_2 in the following section. Make sure you make changes to source files in that module in the Android Studio project.

e096c4d12a3d0a01.png

After you know that the channel is visible, create a PreviewProgram object using Metadata objects with PreviewProgram.Builder. And again, you don't want to insert the same program twice into a channel, so the sample assigns metadata.id to contentId of PreviewProgram for deduplication purposes. Add the following code at TODO: Step 7 create or find an existing preview program.

val existingProgram = existingProgramList.find { it.contentId == metadata.id }
val programBuilder = if (existingProgram == null) {
   PreviewProgram.Builder()
} else {
   PreviewProgram.Builder(existingProgram)
}

Build the builder with media metadata and publish/update it in the channel. (TODO: Step 8 build preview program and publish.)

val updatedProgram = programBuilder.also { metadata.copyToBuilder(it) }
       // Set the same channel ID in all programs
       .setChannelId(channelId)
       // This must match the desired intent filter in the manifest for VIEW action
       .setIntentUri(Uri.parse("https://$host/program/${metadata.id}"))
       // Build the program at once
       .build()

A few things to notice here:

  1. The sample code links metadata with the preview program via its contentId.
  2. Preview program is inserted into a channel by calling setChannelId() on PreviewProgram.Builder().
  3. Android TV system launches the intentUri of a program when the user selects a program from a channel. The Uri should include the program ID so the app can find and play the media from the database when the user selects the program.

Adding programs

Here this codelab uses PreviewChannelHelper from the AndroidX library to insert programs into channels.

Use PreviewChannelHelper.publishPreviewProgram() or PreviewChannelHelper.updatePreviewProgram() to save the program in the channel (at TODO: Step 9 add preview program to channel.)

try {
   if (existingProgram == null) {
       PreviewChannelHelper(context).publishPreviewProgram(updatedProgram)
       Log.d(TAG, "Inserted program into channel: $updatedProgram")
   } else {
       PreviewChannelHelper(context)
               .updatePreviewProgram(existingProgram.id, updatedProgram)
       Log.d(TAG, "Updated program in channel: $updatedProgram")
   }
} catch (exc: IllegalArgumentException) {
   Log.e(TAG, "Unable to add program: $updatedProgram", exc)
}

Nice job! The app now adds programs to channels. You can compare the code with step_3.

Run the app

Select step_2 in the configuration and run the app.

200e69351ce6a530.png

When the app runs, click the Customize Channels button at the bottom of the home screen and look for our app, "TV Classics". Toggle the three channels and watch the logs to see what is happening. Creating channels and programs happens in the background so feel free to add extra log statements to help you trace the events that are triggered.

What you've learned

  • How to add programs to a channel.
  • How to update a program's attributes.

What's next?

Adding programs to Watch Next Channel.

4. Watch Next Channel

The Watch Next channel is located near the top of the home screen; it appears below Apps and above all other channels.

44b6a6f24e4420e3.png

Concepts

The Watch Next channel provides a way for your app to drive engagement with the user. Your app can add the following programs to the Watch Next channel: programs that the user marked as interesting, programs the user stopped watching in the middle, or programs that are related to the content the user is watching (like the next episode in a series or next season of a show). There are 4 types of use cases for the Watch Next channel:

  • Continue watching a video that the user has not finished.
  • Suggest the next video to watch. For example, if the user finished watching episode 1, then you can suggest episode 2.
  • Surface new content to drive engagement.
  • Maintain a watchlist of interesting videos added by the user.

This lesson shows how to use the Watch Next channel to continue watching a video, specifically, how to include a video in the Watch Next channel when the user pauses it. The video should be removed from the Watch Next channel when it plays to the end.

Update playback position

There are a few ways to track the playback position of playing content. This codelab uses a thread to save the latest playback position regularly into the database and to refresh the metadata of the Watch Next program. Please open step_3 and follow the instructions below to add this code.

In NowPlayingFragment, add the following code in the run() method of updateMetadataTask.(at TODO: step 10 update progress):

val contentDuration = player.duration
val contentPosition = player.currentPosition

// Updates metadata state
val metadata = args.metadata.apply {
   playbackPositionMillis = contentPosition
}

The code only saves metadata when the playback position is less than 95% of total duration.

Add the following code (at TODO: step 11 update metadata to database).

val programUri = TvLauncherUtils.upsertWatchNext(requireContext(), metadata)
lifecycleScope.launch(Dispatchers.IO) {
   database.metadata().update(
           metadata.apply { if (programUri != null) watchNext = true })
}

If the playback position has passed 95% of the video, the Watch Next program will be removed to allow other content to be prioritized.

In NowPlayingFragment, add the following code to remove the finished video from the Watch Next row(at TODO: step 12 remove watch next).

val programUri = TvLauncherUtils.removeFromWatchNext(requireContext(), metadata)
if (programUri != null) lifecycleScope.launch(Dispatchers.IO) {
   database.metadata().update(metadata.apply { watchNext = false })
}

The updateMetadataTask is scheduled every 10 seconds to make sure the latest playback position is tracked. It is scheduled in onResume() and stops in onPause() of NowPlayingFragment, so the data only gets updated when the user is watching a video.

Adding/Updating a Watch Next program

TvLauncherUtils interacts with the TV Provider. In the previous step, removeFromWatchNext and upsertWatchNext in TvLauncherUtils are called. Now you need to implement these two methods. AndroidX library provides the PreviewChannelHelper class which makes this task really simple.

First of all, create or find an existing instance of WatchNextProgram.Builder, and then update the object with the latest playback metadata. Add the following code in the upsertWatchNext() method (at TODO: step 13 build watch next program):

programBuilder.setLastEngagementTimeUtcMillis(System.currentTimeMillis())

programBuilder.setWatchNextType(metadata.playbackPositionMillis?.let { position ->
   if (position > 0 && metadata.playbackDurationMillis?.let { it > 0 } == true) {
       Log.d(TAG, "Inferred watch next type: CONTINUE")
       TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE
   } else {
       Log.d(TAG, "Inferred watch next type: UNKNOWN")
       WatchNextProgram.WATCH_NEXT_TYPE_UNKNOWN
   }
} ?: TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_NEXT)

// This must match the desired intent filter in the manifest for VIEW intent action
programBuilder.setIntentUri(Uri.parse(
       "https://${context.getString(R.string.host_name)}/program/${metadata.id}"))

// Build the program with all the metadata
val updatedProgram = programBuilder.build()

After calling the build() method on a WatchNextProgram.Builder, a WatchNextProgam is created. You can publish it to the Watch Next row with PreviewChannelHelper.

Add the following code (at TODO: step 14.1 create watch next program):

val programId = PreviewChannelHelper(context)
       .publishWatchNextProgram(updatedProgram)
Log.d(TAG, "Added program to watch next row: $updatedProgram")
programId

Or if the program exists, just update it (at TODO: step 14.2 update watch next program)

PreviewChannelHelper(context)
       .updateWatchNextProgram(updatedProgram, existingProgram.id)
Log.d(TAG, "Updated program in watch next row: $updatedProgram")
existingProgram.id

Removing a Watch Next program

When a user completes playing the video, you should clean up the Watch Next channel. This is almost the same as removing a PreviewProgram.

Use buildWatchNextProgramUri() to create a Uri that performs a delete. (There's no API we can use in PreviewChannelHelper to remove a Watch Next program.)

Replace the existing code in the removeFromWatchNext()method of TvLauncherUtils class with the statements below (at TODO: step 15 remove program):

val programUri = TvContractCompat.buildWatchNextProgramUri(it.id)
val deleteCount = context.contentResolver.delete(
       programUri, null, null)

Run the app

Select step_3 in the configuration and run the app.

6e43dc24a1ef0273.png

Watch a video from any of your collections for a few seconds and pause the player (spacebar if you're using the emulator). When you return to the home screen, you should see that the movie has been added to the Watch Next channel. Select the same movie from the Watch Next channel and it should continue from where you paused it. Once you watch the entire movie, it should be removed from the Watch Next channel. Experiment with the Watch Next channel using different user scenarios.

What you've learned

  • How to add programs to the Watch Next channel to drive engagement.
  • How to update a program in the Watch Next channel.
  • How to remove a program from the Watch Next channel.

What's next?

After completing the codelab, make the app your own. Replace the media feed and data models with your own and convert them into channels and programs for the TV Provider.

To learn more, visit the documentation!