In this codelab, you'll learn how to build an app that adds channels and programs to the new Android TV home screen. The home screen has more features than this code lab covers. Read the documentation to learn about all the features and capabilities of the new home screen.
Concepts
The home launcher lets your app create custom channels and content that your user can discover. Your app can offer any number of channels for the user to add to the launcher. The user usually has to select and approve each channel before it appears in the launcher. Every app has the option of creating one default channel. The default channel is special because it automatically appears in the launcher; the user does not have to explicitly request it.
The system uses a TV Provider which is a content provider that manages the channels and programs in the home screen. Your app also communicates with the TV Provider when you add and update channels and programs.
The support library has a class called TvContractCompat
that contains constants and builder methods to help you work with channel and program data in a TV Provider.
Overview
This codelab shows how to create, add, and update channels and programs on the launcher screen. It uses a mock database of subscriptions and movies. For simplicity, the same the 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 need to download the source code for this codelab. You can either clone the repository from Github:
git clone https://github.com/googlecodelabs/tv-recommendations.git
...or you can download the repository 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
1-base is the base app that each successive step is based on.
In each step, you'll add more and more code to the 1-base app.
The other modules can be used as checkpoints to compare your work with the solution at each step along the way.
These are the main components in the app:
MainActivity
adds more channels from the UI.SyncChannelJobService
andSyncProgramsJobService
are background jobs that interact with the TV Provider.- model/
MockDatabase
is a mock local movie/subscription database. - model/
MockMovieService
is a mock back end remote service for retrieving movies. - model/
Movie
is an object for storing movie info. - model/
Subscription
is an object for storing subscription info. - playback/
PlaybackVideoFragment
plays movies. - playback/
WatchNextAdapter
interacts with the TV Provider to manage content in the Watch Next channel. - util/
TvUtil
is a helper class to containing boilerplate code for scheduling jobs and adding channels.
Run the starter project
Try running the project. If you have issues, see our documentation on how to get started.
- Connect your Android TV or start the emulator.
- Select the 1-base configuration and press the run button in the menu bar.
- Select your Android device and click OK.
- You should see a simple TV app outline with three buttons.
What you've learned
In this introduction, you've learned about:
- The TV home launcher and its channels
- The codelab 1-base project
What's next?
Adding channels to the launcher.
Start by adding channels to the launcher. Once you have channels, you can add programs to them, and users can discover your channels and select which ones to display in the launcher UI.
You should create all your channels when your app first launches. Use a JobService
to add channels in a background thread. This codelab already schedules a job, SyncChannelJobService
, in the MainActivity
to help you get started.
You'll be adding code to the SyncChannelJobService
class.
The class performs these tasks in the background:
- Retrieve TV channel subscription data from the our mock service.
- Create a channel for each subscription.
- Request that the system makes the channel visible in the launcher. (You will add code to do this to the class later.)
- Save the subscriptions in the database, including a new link to the corresponding channel ID.
- Schedule a background job to watch for updates on the channel to add programs.
Creating a channel
You must convert a Subscription
into a channel and add it to the TV Provider. Be careful to only add each channel once: Query the TV Provider to see if the channel already exists.
Add the following code to the getChannelIdFromTvProvider()
method (at TODO: step 1 query for channel):
Cursor cursor =
context.getContentResolver()
.query(
TvContractCompat.Channels.CONTENT_URI,
new String[] {
TvContractCompat.Channels._ID,
TvContract.Channels.COLUMN_DISPLAY_NAME
},
null,
null,
null);
if (cursor != null && cursor.moveToFirst()) {
do {
Channel channel = Channel.fromCursor(cursor);
if (subscription.getName().equals(channel.getDisplayName())) {
Log.d(
TAG,
"Channel already exists. Returning channel "
+ channel.getId()
+ " from TV Provider.");
return channel.getId();
}
} while (cursor.moveToNext());
}
return -1L;
If the channel did not exists, you must add it. Add the following code to the createChannel()
method (at TODO: step 2 create a channel):
// Checks if our subscription has been added to the channels before.
long channelId = getChannelIdFromTvProvider(context, subscription);
if( channelId != -1L ) {
return channelId;
}
// Create the channel since it has not been added to the TV Provider.
Uri appLinkIntentUri = Uri.parse(subscription.getAppLinkIntentUri());
Channel.Builder builder = new Channel.Builder();
builder.setType(TvContractCompat.Channels.TYPE_PREVIEW)
.setDisplayName(subscription.getName())
.setDescription(subscription.getDescription())
.setAppLinkIntentUri(appLinkIntentUri);
Log.d(TAG, "Creating channel: " + subscription.getName());
Uri channelUrl =
context.getContentResolver()
.insert(
TvContractCompat.Channels.CONTENT_URI,
builder.build().toContentValues());
Log.d(TAG, "channel insert at " + channelUrl);
channelId = ContentUris.parseId(channelUrl);
Log.d(TAG, "channel id " + channelId);
Bitmap bitmap = TvUtil.convertToBitmap(context, subscription.getChannelLogo());
ChannelLogoUtils.storeChannelLogo(context, channelId, bitmap);
return channelId;
The method uses Channel.Builder
to create an instance of the Channel
class. The display name appears on the home screen. The system uses the app link intent Uri to launch the channel when the user selects its channel icon. There is an AppLinkActivity
that handles the delegation of the app link Uris. Currently, the activity shows a toast when the user selects the channel from the launcher. For extra credit, try changing the AppLinkActivity
to deep link into the main activity instead of displaying a toast.
The call to getContentResolver().insert()
inserts the channel's content values into the TV Provider.
Finally, the method sets a logo image for the channel.
Note that the insert method returns a Uri for the channel This can be converted to a channel ID which is the return value for createChannel()
.
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 in the launcher. Every app has the option of creating one default channel. The default channel is special because it automatically appears in the launcher; the user does not have to explicitly request it.
Add the following code to the doInBackground()
method (at TODO: step 3 make the channel visible):
TvContractCompat.requestChannelBrowsable(mContext, channelId);
Scheduling channel updates
A channel without content is useless. This codelab uses a scheduler to add programs to channels. This step only sets up the scheduler. You won't actually add programs until the next step.
Add the following code to the TvUtil
class scheduleSyncingProgramsForChannel()
method (at TODO: step 4 schedule a job):
ComponentName componentName = new ComponentName(context, SyncProgramsJobService.class);
JobInfo.Builder builder =
new JobInfo.Builder(getJobIdForChannelId(channelId), componentName);
JobInfo.TriggerContentUri triggerContentUri =
new JobInfo.TriggerContentUri(
TvContractCompat.buildChannelUri(channelId),
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS);
builder.addTriggerContentUri(triggerContentUri);
builder.setTriggerContentMaxDelay(0L);
builder.setTriggerContentUpdateDelay(0L);
PersistableBundle bundle = new PersistableBundle();
bundle.putLong(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
builder.setExtras(bundle);
JobScheduler scheduler =
(JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
scheduler.cancel(getJobIdForChannelId(channelId));
scheduler.schedule(builder.build());
Since the TV Provider is a content provider, you can set up a JobService
to be triggered upon updates from a particular Uri
. You know each channel's ID, so you can listen to updates for a specific channel.
Run the app
When the app runs, the default channel appears but it does not have any programs.
Compare your code to the solution in the 2-channels directory.
What you've learned
- How to query for channels.
- How to add channels to the launcher.
- How to make a default channel visible.
- How to set a logo on a channel.
- How to schedule a
JobService
to run when a channel is updated.
What's next?
The next section shows to add programs to a channel.
Adding a program to a channel is very similar to creating a channel. Use PreviewProgram.Builder
instead of Channel.Builder
.
You'll be working with the syncPrograms()
method in the SyncProgramsJobService
class.
Verify the channel is visible
The first thing to do is make sure the channel is visible to the user. If the user cannot see the channel, you should not perform extra work that is not visible.
There are helper methods on the Channel
class that convert a cursor's data into an object. To check if a channel is visible on the home screen, use the isBrowsable()
method.
Add the following code to the syncPrograms()
method (at TODO: step 5 check if visible):
try (Cursor cursor =
getContentResolver()
.query(
TvContractCompat.buildChannelUri(channelId),
null,
null,
null,
null)) {
if (cursor != null && cursor.moveToNext()) {
Channel channel = Channel.fromCursor(cursor);
if (!channel.isBrowsable()) {
Log.d(TAG, "Channel is not browsable: " + channelId);
deletePrograms(channelId, movies);
} else {
Log.d(TAG, "Channel is browsable: " + channelId);
if (movies.isEmpty()) {
movies = createPrograms(channelId, MockMovieService.getList());
} else {
movies = updatePrograms(channelId, movies);
}
MockDatabase.saveMovies(getApplicationContext(), channelId, movies);
}
}
}
The code queries the TV Provider for a specific channel. Results are returned in a cursor and converted into a channel object.
If the channel is not currently visible, all the programs associated with the channel are deleted to prevent the data from going stale.
If the channel is visible, check if there are any movies associated with the channel.
If there are no movies on the channel, get a list of movies from the mock service.
Otherwise, update the programs with a fresh list of movies from the mock service.
Create a Preview Program
There are different types of programs you can add to a channel. For starters, create a PreviewProgram.
After you know that the channel is visible, create a PreviewProgram
object using Movie
objects. In the buildProgram()
method, use the PreviewProgram.Builder
to create a PreviewProgram
object and save it in the channel.
Add the following code (at TODO: step 6 convert movie to program):
Uri posterArtUri = Uri.parse(movie.getCardImageUrl());
Uri appLinkUri = AppLinkHelper.buildPlaybackUri(channelId, movie.getId());
Uri previewVideoUri = Uri.parse(movie.getVideoUrl());
PreviewProgram.Builder builder = new PreviewProgram.Builder();
builder.setChannelId(channelId)
.setType(TvContractCompat.PreviewProgramColumns.TYPE_CLIP)
.setTitle(movie.getTitle())
.setDescription(movie.getDescription())
.setPosterArtUri(posterArtUri)
.setPreviewVideoUri(previewVideoUri)
.setIntentUri(appLinkUri);
return builder.build();
The system launches the intentUri
when the user selects a program from a channel in the launcher. The Uri
should include the channel ID and movie ID to so the app can find and play the movie from the mock database when the user selects the program.
Set the poster art so each program has something to display in the channel. You can also set a thumbnail image for when a program is selected a different image may be shown. To experiment with the different images add the following line to the builder:
builder.setThumbnailUri(Uri.parse(movie.getBackgroundImageUrl()))
Set the preview video Uri so that when a program is selected, the home screen starts to play content. This is a great way to add a video description to your programs. This codelab reuses the actual video for the preview.
For best results, do not use both a thumbnail and a preview video at the same time.
Adding programs
Now that you can convert a Movie
into a PreviewProgram
, you can fetch the data for all the movies in a subscription and add them as programs to the corresponding channel.
To create a program in the channel you must convert a movie to a program and insert it into the TV Provider. Store the program ID that's returned in order to perform updates later.
Add the following code in the createPrograms()
method (at TODO: step 7 add programs):
List<Movie> moviesAdded = new ArrayList<>(movies.size());
for (Movie movie : movies) {
PreviewProgram previewProgram = buildProgram(channelId, movie);
Uri programUri =
getContentResolver()
.insert(
TvContractCompat.PreviewPrograms.CONTENT_URI,
previewProgram.toContentValues());
long programId = ContentUris.parseId(programUri);
Log.d(TAG, "Inserted new program: " + programId);
movie.setProgramId(programId);
moviesAdded.add(movie);
}
return moviesAdded;
The app now adds programs to channels.
Updating programs
Updating a program is very similar to adding it. There are several attributes that you can set on a program such as the episode number, price, and rating just to name a few. If these attributes change you can update the program to reflect the changes.
Add the following code in the updatePrograms()
method (at TODO: step 8 update programs):
// By getting a fresh list, the update of the home screen is
// clearly visible.
List<Movie> updateMovies = MockMovieService.getFreshList();
for (int i = 0; i < movies.size(); ++i) {
Movie old = movies.get(i);
Movie update = updateMovies.get(i);
long programId = old.getProgramId();
getContentResolver()
.update(
TvContractCompat.buildPreviewProgramUri(programId),
buildProgram(channelId, update).toContentValues(),
null,
null);
Log.d(TAG, "Updated program: " + programId);
update.setProgramId(programId);
}
return updateMovies;
Each fresh list of movies is just a shuffled list of the mock data. The for loop walks through both movie lists together and updates the old movie's program reference with the new movie's data.
When you perform an update, you should pay attention to the user's preferences and remove a program if they've marked it to not be shown again.
Deleting programs
If a channel is not visible you should delete its programs so that the channel's content does not become stale if the user makes the channel visible in the future.
Deleting is very straightforward. Call delete()
on the program's Uri. Add the following code to the deletePrograms()
method (at TODO: step 9 delete programs)
int count = 0;
for (Movie movie : movies) {
count +=
getContentResolver()
.delete(
TvContractCompat.buildPreviewProgramUri(movie.getProgramId()),
null,
null);
}
Log.d(TAG, "Deleted " + count + " programs for channel " + channelId);
Run the app
When the app runs, go into Manage Channels at the bottom of the home screen and look for our app, "Codelab Channels and Programs". 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.
Compare your code to the solution in the 3-programs directory.
What you've learned
- How to add programs to a channel.
- How to update a program's attributes.
- How to delete a program from a channel.
What's next?
How to keep your users engaged by surfacing content on the Watch Next channel.
The Watch Next channel belongs to the launcher and 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. There are several types of use cases for this 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: 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.
Player Callbacks
Start by hooking into the player's callbacks. The app listens for state change events in SimplePlaybackTransportControlGlue
. There are several methods that can be overridden in the callback. You need to override onPlayStateChanged()
and onPlayCompleted()
.
In PlaybackVideoFragment
, add the following code in the onPlayStateChanged()
callback method (at TODO: step 10 update progress):
long position = mMediaPlayerGlue.getCurrentPosition();
long duration = mMediaPlayerGlue.getDuration();
watchNextAdapter.updateProgress(
getContext(), mChannelId, movie, position, duration);
onPlayStateChanged()
is called whenever the video changes states from play to pause and vise versa. You can check the state by calling glue.isPlaying()
. You need to update the progress in the Watch Next channel regardless of the state, so that when the user continues watching it starts where they left off.
Once the video completes playback, you should remove it from the Watch Next channel.
Add the following code in the onPlayCompleted()
method (at TODO:step 11 remove watch next):
watchNextAdapter.removeFromWatchNext(
getContext(), mChannelId, movie.getId());
Adding a program to the Watch Next channel
WatchNextAdapter
interacts with the TV Provider. In the previous step you hooked into the callbacks, now you need to actually add a program to the Watch Next channel.
Add the following code in the updateProgress()
method (at TODO: step 12 add watch next program):
WatchNextProgram program = createWatchNextProgram(channelId, entity, position, duration);
if (entity.getWatchNextId() < 1L) {
// Need to create program.
Uri watchNextProgramUri =
context.getContentResolver()
.insert(
TvContractCompat.WatchNextPrograms.CONTENT_URI,
program.toContentValues());
long watchNextId = ContentUris.parseId(watchNextProgramUri);
entity.setWatchNextId(watchNextId);
MockDatabase.saveMovie(context, channelId, entity);
Log.d(TAG, "Watch Next program added: " + watchNextId);
} else {
// TODO: step 14 update program.
}
Before you can run the app, you must implement the createWatchNextProgram()
method to convert a Movie
to a WatchNextProgam
.
Add the following code (at TODO: step 13 convert movie):
Uri posterArtUri = Uri.parse(movie.getCardImageUrl());
Uri intentUri = AppLinkHelper.buildPlaybackUri(channelId, movie.getId(), position);
WatchNextProgram.Builder builder = new WatchNextProgram.Builder();
builder.setType(TvContractCompat.PreviewProgramColumns.TYPE_MOVIE)
.setWatchNextType(TvContractCompat.WatchNextPrograms.WATCH_NEXT_TYPE_CONTINUE)
.setLastEngagementTimeUtcMillis(System.currentTimeMillis())
.setLastPlaybackPositionMillis((int) position)
.setDurationMillis((int) duration)
.setTitle(movie.getTitle())
.setDescription(movie.getDescription())
.setPosterArtUri(posterArtUri)
.setIntentUri(intentUri);
return builder.build();
Add the position to the end of the intentUri
to indicate where to continue playing. The watch next type and last engagement time are required. It is used to sort the programs in the channel. The program with the most recent last engagement time appears at the beginning of the channel.
Updating a program in the Watch Next Channel
Every time the user pauses a video, you must update the last playback position in the program and in the intent Uri so that the video continues from the right place. In the previous step, you added a new WatchNextProgram
, this time you'll update an existing program.
Update the WatchNextProgram
just like a PreviewProgram
, but use the buildWatchNextProgramUri()
method from the TvContractCompat
instead.
Add the following in the else
clause that you added to the updateProgress()
method earlier (at TODO: step 14 update program):
// Update the progress and last engagement time of the program.
context.getContentResolver()
.update(
TvContractCompat.buildWatchNextProgramUri(entity.getWatchNextId()),
program.toContentValues(),
null,
null);
Log.d(TAG, "Watch Next program updated: " + entity.getWatchNextId());
Removing a program from the Watch Next Channel
When a video completes playing, you should clean up the Watch Next channel. This is almost the same as removing PreviewPrograms
. Use the buildWatchNextProgramUri()
to create a Uri
that performs a delete.
Add the following code in the removeFromWatchNext()
method (at TODO: step 15 remove program):
int rows =
context.getContentResolver()
.delete(
TvContractCompat.buildWatchNextProgramUri(movie.getWatchNextId()),
null,
null);
Log.d(TAG, String.format("Deleted %d programs(s) from watch next", rows));
Run the app
Select a movie from one of your channels and pause the player (spacebar if you're using the emulator). When you return home you should see 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 and imagine all of the possibilities!
Compare your code to the solution in the 4-watch-next directory.
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?
Adding more channels from your app.
The user controls the channels they want to see. The interaction with the user is similar to the way they accept Android permissions.
If the user likes specific content from your app, they can personalize their home screen by adding your channels. For example, clicking the "Subscribe" button in the screenshot above adds the "Google Developers" channel to the user's home screen. The new channelappears at the bottom of the screen. To add a channel from your app, follow these steps:
- Provide some UI element that lets the user add one or more channels.
- Create a channel in the provider.
- Prompt the user to allow adding the channel to the home screen.
- Handle the user's response.
Provide a UI element
The MainActivity
in the codelab has three buttons that simulate adding channels from the UI.
The OnClickListener
displays a dialog asking the user to approve adding the channel. Once the channel is approved, you must add it in the background.
Create a new channel
When you add a channel in-app, you need to create a new channel as you did above. Similar create code already exists in the TvUtil.createChannel()
convenience method, so you don't need to write it again.
You can trigger an AsyncTask
from the onClick()
method to interact with the TV Provider in the background while prompting the user in the foreground.
In MainActivity,
add the following code to the doInBackground()
method of your AsyncTask (at TODO: step 16 create channel):
long channelId = TvUtil.createChannel(mContext, subscription);
Prompt the user
After adding the channel, you need to tell the system to display it in the home screen. In the onPostExecute()
the app asks the system to display the channel. However, before this can happen the user must approve. To do this, start a new activity that asks for permission to add the channel. The activity result tells you if the user approved.
Add the following code in the promptUserToDisplayChannel()
method (at TODO: step 17 prompt user):
Intent intent = new Intent(TvContractCompat.ACTION_REQUEST_CHANNEL_BROWSABLE);
intent.putExtra(TvContractCompat.EXTRA_CHANNEL_ID, channelId);
try {
this.startActivityForResult(intent, MAKE_BROWSABLE_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Could not start activity: " + intent.getAction(), e);
}
Handle the user's Response
Since you launched an activity for a result, you can check the user's response by evaluating the result code. If the user accepted adding the channel, you'll get RESULT_OK, otherwise they chose not to add the channel to their home screen.
Add the following code in the onActivityResult()
method (at TODO: step 18 handle response):
if (resultCode == RESULT_OK) {
Toast.makeText(this, R.string.channel_added, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, R.string.channel_not_added, Toast.LENGTH_LONG).show();
}
Optional
For extra credit, change the enabled state of the buttons based on the user's response. If the user chooses to add a channel, disable the button since there is no reason to allow the user to add the channel again once it's already on the screen.
Run the app
Congratulations! You've completed the codelab.
Compare your code to the solution in the 5-opt-in-channels directory.
What you've learned
- How to add channels to the home screen.
- How to manage programs in your channels.
- How to surface more content in the special Watch Next channel.
- How to dynamically add more channels from your app's UI.
What's next?
After completing the codelab, make the app your own. Replace the Subscription
and Movie
classes with your own data model and convert them into channels and programs for the TV Provider.
To learn more, visit the documentation!