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.
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.
Understanding the starter project
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.
- Connect your Android TV or start the emulator.
- Select the step_1 configuration, select your Android device and press the run button in the menu bar.
- You should see a simple TV app outline with three collections of videos.
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:
- Fetches the media feed, which includes background images, media collections, and video metadata. This information is defined in
assets/media-feed.json
- Updates the
TvMediaDatabase
instance, which stores background images, media collections, and video data, to its respective objects - 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.
- 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 theinternalProviderId
of a channel. To identify an existing channel, compare itsinternalProviderId
to the collection id. Copy and paste the following code intoupsertChannel()
at the code comment //TODO: Step 1 create or find an existing channel.
val channelBuilder = if (existingChannel == null) {
PreviewChannel.Builder()
} else {
PreviewChannel.Builder(existingChannel)
}
- 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 theappLinkIntentUri
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()
- Call functions in the
PreviewChannelHelper
class to insert the channel into the TV provider or update it. The call topublishChannel()
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
- 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.
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.
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.
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:
- The sample code links metadata with the preview program via its
contentId
. - Preview program is inserted into a channel by calling
setChannelId()
onPreviewProgram.Builder()
. - Android TV system launches the
intentUri
of a program when the user selects a program from a channel. TheUri
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.
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.
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.
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!