Blog Infos
Author
Published
Topics
Published

How nice would it be to display your app content before the user thinks about opening it? Guess what? This is possible with the Channels API!

In this post, we will go through a step-by-step guide with some helpful insights on creating and publishing your app’s Preview Channels to the Android TV home screen and how to handle user interaction with your channels. Make sure to check out the sample app with all the concepts of this post.

Preview Channels

Android TV supports various channels, from ATSC and DVB channels, i.e., cable channels, to other standards of satellite channels. Today we’ll discuss a specific channel type called Preview Channels.

You might have already seen a few if you’re familiar with the Android TV home screen. Every time you install an app, and you can discover the app’s content on the home screen, chances are that you’re seeing a Preview Channel. Preview Channels can be understood simply as rows of content that your app can add to the tv home screen.

Here’s the opening from the official documentation:

“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.”

Keep your users engaged!

Having relevant and updated content on the home screen is an effective way to keep your users engaged with your app. Sometimes, the only thing apart from the user returning to your app is visual encouragement. That’s why Preview Channels are so appealing. On a glance at the home screen, the user is one click away from consuming your app’s content.

Note: Preview Channels are only available in Android TV 8.0 (API 26) and later. If you want to support a similar feature in older versions, look at the Recommendations Row documentation.

How does it work?

Apps can perform all CRUD (Create, Read, Update, and Delete) operations in channels using the TvProvider and ContentProvider APIs (we’ll cover those in more detail later). Still, to show the channels on the home screen, the app must ask the user’s permission, except for the first published channel.

A channel is not published, i.e., visible on the home screen as soon as it’s created. For that, we must ask the user permission to publish the channel.

The first channel created by your app can be published without asking for the user’s permission. We’ll call the first channel the “default” channelWith this in mind, it is important never to delete your default channel. Otherwise, we’ll need to ask the user permission to publish it again. You should instead repurpose it, changing its information and content, and never delete and recreate it.

Programs

Channels without content aren’t much. They’re not even displayed on the home screen. To add content to channels, we need to create Programs. A program will become a card on a channel row.

CRUD operations on programs are done using the same TvProviderand ContentProvider APIs mentioned above. After creating programs, we should add them to a channel to make their content available to the users.

You can provide a lot of information about your programs, such as title, description, type, genre, video preview URLthumbnail, duration, and the list goes on… Depending on the type of your program (if it’s a movie, a series episode, a season, or any other kind), some pieces of information are more relevant than others. Keep this in mind when creating your programs to provide the best experience for your users. You can check the complete list of program attributes to decide which ones to provide here.

After creating programs, adding them to channels, and publishing them, they’ll be visible on the home screen, where users can quickly deep-link to your app’s home or straight to a video/content playback.

💡 ️Tip: Consider how your content can be structured in this channel/program hierarchy before writing any code.

Enough with theory. Please show me the code!

Start by adding the TvProvider dependency to your app’s build.gradle.

dependencies {
    ...
    implementation 'androidx.tvprovider:tvprovider:1.1.0-alpha01'
}
Creating or Updating a Channel

The implementations to create or update a channel are very similar.

val existingChannel = queryChannels().firstOrNull {
it.internalProviderId == category.id.toString()
}
val channelBuilder = when (existingChannel) {
null -> Channel.Builder()
else -> Channel.Builder(existingChannel)
}
val channel = channelBuilder
.setType(TvContractCompat.Channels.TYPE_PREVIEW)
.setDisplayName(category.title)
.setInternalProviderId(category.id.toString())
.setAppLinkIntentUri("content://channelsample.com/category/${category.id}".toUri())
.build()
view raw Channels.kt hosted with ❤ by GitHub

Note: We will cover the queryChannels() function later. For now, assume that it returns all channels of your app.

After building the channel, we need to use the ContentResolver to insert or update it on the database which is used by the system to get information about the channels.

when (existingChannel) {
null -> context.contentResolver.insert(
TvContractCompat.Channels.CONTENT_URI,
channel.toContentValues()
)
else -> context.contentResolver.update(
TvContractCompat.Channels.CONTENT_URI,
channel.toContentValues(),
null,
null
)
}
view raw Channel.kt hosted with ❤ by GitHub
Publishing a channel

As mentioned, creating a channel is not enough to make it visible on the home screen. We need to ask for the user’s permission to publish the channel. The code for this is very straightforward:

try {
val intent = Intent(TvContractCompat.ACTION_REQUEST_CHANNEL_BROWSABLE)
intent.putExtra(TvContractCompat.EXTRA_CHANNEL_ID, channelId)
startActivityForResult(intent, REQUEST_CHANNEL_BROWSABLE)
} catch (exception: ActivityNotFoundException) {
// Handle exception
}
view raw Channels.kt hosted with ❤ by GitHub

This will prompt the user with a dialog asking to add the channel to the TV home screen.

The default channel

To create the default channel, we will use a BroadcastReceiver usually triggered once your app is installed. In rare cases, it can be triggered only on the first app launch. Add the following to your AndroidManifest.xml

<receiver
android:name=".InitProgramsBroadcastReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.media.tv.action.INITIALIZE_PROGRAMS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>

And here’s how we can implement the BroadcastReceiver:

class InitChannelsBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (defaultChannelAlreadyAdded) {
// Make sure we are not trying to re-create the channel if it's already added.
// You can save this on your shared preferences or database.
return
}
val channelHelper = PreviewChannelHelper(context)
val channel = Channel.Builder()
.setType(TvContractCompat.Channels.TYPE_PREVIEW)
.setInternalProviderId("default_channel")
.setDisplayName("ChannelSample: Hand picked recommendations!")
.setAppLinkIntentUri("content://channelsample.com/discover".toUri())
.build()
val channelUri = context.contentResolver.insert(
TvContractCompat.Channels.CONTENT_URI,
channel.toContentValues()
)
if (channelUri != null) {
val channelId = ContentUris.parseId(channelUri)
val myChannels = channelHelper.allChannels.filter {
it.packageName == context.packageName
}
// If there are no browsable channels, i.e., channels visible on the TV Home screen, we can
// make the first channel browsable without asking the user permission.
if (myChannels.none { it.isBrowsable }) {
TvContractCompat.requestChannelBrowsable(context, channelId)
}
MovieList.list.forEach { movie ->
val program = movie.toPreviewProgram(channelId)
channelHelper.publishPreviewProgram(program)
}
}
}
}

💡 Tip: Notice the call to Channel.setInternalProviderId() function. This is a way to set the id your app uses to identify the channel. As the channel id needs to be unique among all the installed apps, it is generated by the database, and we can’t change it. That’s why we have the setInternalProviderId() API to store your app’s internal id for that channel.

To test your BroadcastReceiver implementation, you can trigger its Intent via adb with the following code:

adb shell am broadcast -a android.media.tv.action.INITIALIZE_PROGRAMS -n \
    your.package.name/.YourReceiverName
Querying channel information

To manage the app channels, we usually need to query for their data to decide what information needs to change, what programs we need to add or remove, and what shouldn’t be touched.

There are a few ways and APIs to help us with that. If you want complete control and access to all your channel data, use the ContentResolver API and query the channels database for the exact information you need. If you only need to read the basics about your channel, you can use the PreviewChannelHelper API.

PreviewChannelHelper

With PreviewChannelHelperyou have out-of-the-box implementations of the most common CRUD operations with channels and programs. It is an abstraction layer on top of the low-level operations that can be performed with the ContentResolver. Take a look at the documentation for the available functions.

When reading channel data with the PreviewChannelHelper, you can only read the columns listed on the PreviewChannelHelper.Columns.PROJECTION array which are:

public static final String[] PROJECTION = {
Channels._ID,
Channels.COLUMN_PACKAGE_NAME,
Channels.COLUMN_TYPE,
Channels.COLUMN_DISPLAY_NAME,
Channels.COLUMN_DESCRIPTION,
Channels.COLUMN_APP_LINK_INTENT_URI,
Channels.COLUMN_INTERNAL_PROVIDER_ID,
Channels.COLUMN_INTERNAL_PROVIDER_DATA,
Channels.COLUMN_INTERNAL_PROVIDER_FLAG1,
Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
Channels.COLUMN_INTERNAL_PROVIDER_FLAG3,
Channels.COLUMN_INTERNAL_PROVIDER_FLAG4
};

A channel has plenty more attributes than this. Take a look at the getProjection() function in its source code to see all the existing attributes.

A very useful attribute is the Channels.COLUMN_BROWSABLE, which indicates if the channel is already published or not. With that, you can show or hide a button to remove the channel from the TV home screen from inside your app. To query that value from the database though, you must use the ContentProvider.

💡 Tip: To avoid frustrations and lost time debugging things like a channel property value not making sense, e.g., a channel that is already published having the Channel.isBrowsable property being false, be aware that the PreviewChannelHelper fills the Channel instance variables with the result of the query from the PROJECTION shown above, which has very limited information. In other words, properties not listed on the projection will have the field default value (false for booleans, 0 for numbers, and so on…)

ContentProvider

Using the ContentProvider APIs gives you full control of the data you want to query. If you want, for example, to query all attributes from a channel you can do the following:

private fun queryChannels(): List<Channel> {
val channels = ArrayList<Channel>()
context.contentResolver.query(
/* uri = */ TvContractCompat.Channels.CONTENT_URI,
/* projection = */ Channel.PROJECTION,
/* selection = */ null,
/* selectionArgs = */ null,
/* sortOrder = */ null
).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
channels.add(Channel.fromCursor(cursor))
}
}
return channels
}
view raw Channels.kt hosted with ❤ by GitHub

💡 Tip: Check the implementations of PreviewChannelHelper functions and adjust them for your needs.

Creating or Updating a Program

In the same way we manage channels, managing programs is very similar. Look at how we can create or update an existing program and add it to a channel.

fun insertOrUpdateChannelPrograms(context: Context, channelId: Long) {
val existingPrograms = getChannelPrograms(channelId)
MovieList.list.forEach { movie ->
val existingProgram = existingPrograms.find {
it.internalProviderId == movie.id.toString()
}
val programBuilder = when (existingProgram) {
null -> PreviewProgram.Builder()
else -> PreviewProgram.Builder(existingProgram)
}
val program = programBuilder.setChannelId(channelId)
.setType(TvContractCompat.PreviewPrograms.TYPE_MOVIE)
.setTitle(title)
.setPreviewVideoUri(videoUrl?.toUri())
.setDurationMillis(Random.nextInt(0, 10) * 60 * 1000)
.setLastPlaybackPositionMillis(0)
.setGenre(genre)
.setDescription(description)
.setPosterArtUri(cardImageUrl?.toUri())
.setIntentUri("content://channelsample.com/movie/$id".toUri())
.setInternalProviderId(id.toString())
.build()
when (existingProgram) {
null -> context.contentResolver.insert(
/* url = */ TvContractCompat.PreviewPrograms.CONTENT_URI,
/* values = */ program.toContentValues()
)
else -> context.contentResolver.update(
/* uri = */ TvContractCompat.PreviewPrograms.CONTENT_URI,
/* values = */ program.toContentValues(),
/* where = */ null,
/* selectionArgs = */ null
)
}
}
}
view raw Program.kt hosted with ❤ by GitHub

💡 Tip: Notice the call to PreviewProgram.Builder.setInternalProviderId() function. It follows the same logic as the channel builder by providing a way to store the app internal content id into a program.

⚠️ Attention: TheContentProvider CRUD operations are I/O operations. To have the best performance on your app, make sure you are running these operations outside the main thread.

To wrap up the program management section, check out the implementation of the getChannelPrograms() function.

fun getChannelPrograms(channelId: Long): List<PreviewProgram> {
val programs = ArrayList<PreviewProgram>()
context.contentResolver.query(
/* uri = */ TvContractCompat.PreviewPrograms.CONTENT_URI,
/* projection = */ PreviewProgram.PROJECTION,
/* selection = */ null,
/* selectionArgs = */ null,
/* sortOrder = */ null
).use { cursor ->
if (cursor != null) {
while (cursor.moveToNext()) {
val program = PreviewProgram.fromCursor(cursor)
if (program.channelId == channelId) {
programs.add(program)
}
}
}
}
return programs
}
view raw Programs.kt hosted with ❤ by GitHub
Handling User Actions on programs and channels

Handling user actions is basically handling App Links. When the user clicks on a channel or program, an Intent will be sent to your app with the URI provided on the channel/program creation. After that, it’s basically navigating to that piece of content on your app.

Start by adding an <intent-filter> to an <activity> on your app’s AndroidManifest.xml like the following:

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:host="channelsample.com" android:pathPrefix="/discover"/>
<data android:host="channelsample.com" android:pathPrefix="/category"/>
<data android:host="channelsample.com" android:pathPrefix="/movie"/>
</intent-filter>

And add some code to handle the Intent on your Activity or Fragment for example:

fun handleIntent(intent: Intent) {
val intentAction = intent.action
if (intentAction == Intent.ACTION_VIEW) {
val intentData = intent.data
val pathSegments = intentData?.pathSegments ?: emptyList()
when (pathSegments.firstOrNull()) {
"movie" -> pathSegments.get(1)?.let { movieId ->
val movie = MovieList.list.firstOrNull { it.id.toString() == movieId }
if (movie != null) {
val movieIntent = Intent(context, PlaybackActivity::class.java)
movieIntent.putExtra(PlaybackVideoFragment.MOVIE, movie)
startActivity(movieIntent)
}
}
"category" -> pathSegments.getOrNull(1)?.let { categoryId ->
selectRowForCategory(categoryId)
}
"discover", null -> {
// Just open the app. The user can browse content on the main screen
}
}
}
}
view raw Fragment.kt hosted with ❤ by GitHub
That’s another wrap!

If you got to this point, thank you for reading this post! I’m sure you’re striving to build (or already built) an awesome app for your users!

Follow me to be notified about the next posts. You can also reach me on Twitter with questions or suggestions at @admqueiroga

Additional resources:

  1. Make sure to surf through the official and amazing documentation on channels. There’s a lot of knowledge in there.
  2. Check out the channels codelab.
  3. Check the App-Links documentation.
  4. Take a look at the sample app I used to learn and get inspiration for this post

See you soon!

This article was previously published on proandroiddev.com

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
In the first part of this series, we learned about the building blocks of…
READ MORE
blog
Since August 5, 2020 (decree № 2020–983), segmented advertising has been authorized on television…
READ MORE
blog
In this post, we’ll learn about Android MediaSession API, why we should use it,…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu