Sie sind auf Seite 1von 48

1.

Concepts and setup


In this codelab, you'll learn how to quickly enable a mobile app for Android TV using the Leanback
library. At the end of the codelab you can expect to have a UX compliant single apk for mobile
devices and Android TV.

Concepts
To start off let's learn a little bit about Android TV. What is Android TV and how is it different? At its
core, it is Android so most of the things that you've learned developing your mobile app can be
reused. The key difference is input and the presentation of information.
Android TV is designed for the 10 foot experience. Instead of using a touchscreen, users will be
navigating your app using a controller while sitting far away from the screen. Instead of swiping the
notification bar down, the notifications will be displayed as the top row of cards. And, the screen is
always filled with rich visual content.

In an effort to simplify integration for developers we created the Leanback library. Leanback has
extendable fragments to allow you to quickly and easily create rich animated experiences. The core
fragments we'll be working with are:

 BrowseFragment - Browse through a video library


 DetailsFragment - Display the details of a specific video
 PlaybackOverlayFragment - Control video playback

These fragments use the Model View Presenter pattern. You'll bind your data model to the view
using presenter classes.

There's a lot of ground to cover, so let's get started!

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.

The first thing we need to do is get the mobile app to build on. Open up your Terminal and run:

git clone https://github.com/googlecodelabs/android-tv-leanback.git

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

checkpoint_0 is the base app that we'll be building upon.

You will be adding code from each step to checkpoint_0.

Each of the following checkpoints can be used as reference points to check your work or for help if
you encounter any issues. The checkpoint number corresponds with the codelab step - 1. (0 vs 1
indexing)

A brief overview of each of the components:

 MainActivity - Video browser


 PlayerActivity - Video player
 SlidingTabLayout, SlidingTabStrip, VideoItemFragment - UI for video browser
 data/ Video - Object storing video info
 data/ VideoContentProvider, AbstractVideoItemProvider, VideoItemContract, VideoDataManager
- Mock local video database

Running the starter project


Let's run it on a phone.

 Connect your Android device or start an emulator.

 Select the checkpoint_0 configuration and click run.


 Select your Android device and click OK.

The application stores all its data in a ContentProvider backed up by a SQLite database. If you want to
learn more about querying databases take a look at ourdocumentation.

Here's what it should look like:


Now let's see how it looks on Android TV.

ADB connect to Android TV


First we need to connect to the Android TV device. In order to that you can use a male to male USB
cable or adb connect.

On the Android TV

Let's enable developer mode if it's not yet enabled.

 Open Settings
 Under preferences open Developer Options
 If Developer Options doesn't display go into Device -> About, scroll down to Build and click
the build number a few times until you receive the "You're A Developer" toast. Then exit out
of Settings and open Settings up again. Click Debugging.
 Change USB debugging to On

If you have a USB cable, connect your Android TV device directly to your machine. You should now
be ready to run the app. You can test if it worked by running the following on your development
machine:

adb devices

If your device is listed, you're now in business and can skip to running the app! If that doesn't work,
try the steps below.

Find the IP of the device.

 Back out to the main Settings page and click Device.


 Select the method that you've used to connect the device to your intranet.
 If you're using Wi-Fi, select Wi-Fi and then select the network you're currently connected to.
 Click on Status info and record your IP.
 If you're using ethernet select the Internet connection and record your IP.
On your development machine

Once you have the IP of the device, you can connect to it using adb connect in a terminal.

adb connect [ip address]:4321

Great, we're now connected!

Run the app on Android TV


Let's run the app on Android TV. In Android Studio select checkpoint_0 and click run again. This
time the Android TV device should appear in the list of running devices. Select it and click ok.
Now the mobile app is running on Android TV. It could use some TV UI love right? In the next few
steps we'll cover adding some Android TV UI to the existing data sources and video player.
Summary
In this step you've learned about:

 Android TV and its concepts


 The codelab checkpoint_0 base
 How to connect to Android TV devices and deploy APKs

Next up
Let's start creating the video browsing experience.

2. Creating a skeleton video browse activity


In this step we'll put up the framework for the video browser fragment. The key concepts to take
away in this step are:

 Leanback dependencies
 Android manifest update for Android TV
 Extending the Leanback BrowseFragment

Let's get started.

Leanback dependencies
Open the build.gradle (checkpoint_0) file.

Under dependencies, notice that the following dependencies exist:

build.gradle
compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.android.support:leanback-v17:23.3.0'
compile 'com.android.support:recyclerview-v7:23.3.0'

There are a few things to note here. The Leanback libraries that target API version 23 are
backwards compatible to API version 17. For apps that require support for previous versions of
Android, you should make sure that the code path utilizing libraries with higher minSdk does not run
on devices with version < minSdk (library).

Next, let's take create a browsing activity.

Create a browsing activity


In the Android Studio project explorer create a new package under com.android.example.leanback by
right-clicking the folder and clicking New -> Package.
Name the new package fastlane.

Under fastlane right-click to create a new Blank Activity called LeanbackActivity, and click on Finish.

Once the class is created, delete the menu/menu_leanback.xml resource since it won't be used.
In theLeanbackActivity class, delete the onCreateOptionsMenu and onOptionsItemSelected functions.
Also set the LeanbackActivity class to extend Activity instead of ActionBarActivity.

Declare launcher activity for Android TV


Open up AndroidManifest.xml

First, declare that we want to use leanback. As a child of manifest add the following line:

AndroidManifest.xml
<uses-feature android:name="android.software.leanback"
android:required="false" />

Add an intent filter to the LeanbackActivity tag. android.intent.category.LEANBACK_LAUNCHER tells


Android TV to launch LeanbackActivity when the application is run.

AndroidManifest.xml
<activity
android:name=".fastlane.LeanbackActivity"
android:label="@string/title_activity_player"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>

We are also adding a theme to the activity. In the next step we will create a values-television
directory and create values specific to television.

Handling unsupported hardware features

Since certain features are not available on TV, you need to define their requirement as optional. If
you use any of the following features you'll need to add android:required="false" to the definition.

Hardware Android feature descriptor

Touchscreen android.hardware.touchscreen

Telephony android.hardware.telephony

Camera android.hardware.camera

Near Field Communications (NFC) android.hardware.nfc

GPS android.hardware.location.gps

Microphone android.hardware.microphone

<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />

Create resource values for television


We want to create some values that are specifically for Android TV.

→ Right click on the res directory and create a new Android resource directory.
→ Create a new values resource directory add UI mode and select Television as the qualifier.

→ Right click on the res directory and create a new value resource file under the newly created
values-television. Name the resources file styles.xml.

→ Add the following styles which inherit from the Leanback theme to customize the look and feel.

<style name="AppTheme" parent="Theme.Leanback">


<item name="colorPrimary">@color/primary</item>
<item name="colorAccent">@color/accent</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="imageCardViewStyle">@style/MyImageCardViewStyle</item>
<item name="headerStyle">@style/MyHeaderStyle</item>
<item name="rowHeaderStyle">@style/MyHeaderStyle</item>
</style>

<style name="MyImageCardViewStyle" parent="@style/Widget.Leanback.ImageCardViewStyle">


<item name="cardType">infoUnderWithExtra</item>
<item name="infoAreaBackground">@color/primary_dark</item>

</style>

<style name="MyHeaderStyle" parent="@style/Widget.Leanback.Header" >


<item name="android:textAppearance">@style/MyHeaderStyle.MyHeaderText</item>
</style>

<style name="MyHeaderStyle.MyHeaderText" parent="TextAppearance.Leanback.Header">


<item name="android:textSize">@dimen/lb_browse_header_text_size</item>
<item name="android:textColor">@color/accent</item>
<item name="android:textAllCaps">true</item>
<item name="android:textStyle">bold</item>
</style>

We've included Leanback libraries, and now Android TV will launch into the correct activity. Let's
create the video browser.
Create a fragment that extends BrowseFragment
We'll leverage the Leanback BrowseFragment. The BrowseFragment class in the Leanback library
allows you to create a primary layout for browsing categories and rows of media items with a
minimum amount of code.

The first step is to create a class that extends BrowseFragment.

→ Under fastlane create a new empty class called LeanbackBrowseFragment that


extends BrowseFragment.

Next let's fill out the class a little bit.

→ To the class, add a private member ArrayObjectAdapter. We'll get into details
about ArrayObjectAdapter in the next step.

private ArrayObjectAdapter mRowsAdapter;

→ We'll also add a helper function to initialize the fragment. In it we instantiate mRowsAdapter, set it
as the Adapter for the fragment then set our main color and badge which appears in the top right of
the browse view.

public void init() {


mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
setAdapter(mRowsAdapter);

setBrandColor(ContextCompat.getColor(getContext(), R.color.primary));
setBadgeDrawable(ContextCompat.getDrawable(getContext(), R.drawable.filmi));
}

→ Override the onViewCreated method and call init.

@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
init();
}

Alright, onto the final step of this section, adding this fragment to the activity.

Add fragment to activity


Open up activity_leanback.xml under layout and delete everything. The only thing we'll display in this
activity is our LeanbackBrowseFragment fragment. So add the following:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_frame"
android:layout_width="match_parent"
android:layout_height="match_parent">

<fragment
android:name="com.android.example.leanback.fastlane.LeanbackBrowseFragment"
android:id="@+id/browse_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</FrameLayout>

Congrats! The framework for the browse fragment is in place. The checkpoint_1 directory contains
all the changes described above in case you got stuck or want to compare notes.

Summary
In this step you've learned about:

 Declaring dependencies
 Updating the Android manifest
 Extending the BrowseFragment

Next up
3. Loading content into the
Let's finish the browse fragment.

video browse fragment


In this step we'll learn about how the Leanback BrowseFragment works and put some content into it.
Let's get started.

Concepts
First, let's cover how the BrowseFragment works. The BrowseFragment basically renders rows of data
that you provide.

Think of each row as two pieces, a HeaderItem which defines the category and an array of objects
represented by the ListRow class which defines the content.

The ArrayObjectAdapter is an array of the defined ListRows that aggregates the rows for
the BrowseFragment view.

We can store any sort of View in ListRows, but in our app we'll use the Leanback ImageCardView.
The zoom and additional detail effects are automatically handled by the Leanback library.
To tie your video data and the ImageCardView together, we use a Presenter. The Presenter defines
which elements of the view are populated from which elements of the model.

Lastly we have the ViewHolder which is a container for the created view.

Let's put all of these concepts together to create the video browsing experience.

Create a presenter
We need to create a presenter to tie our Video model to the ImageCardView.

→ Under fastlane create a new class called CardPresenter that extends Presenter.

→ Define class variables to store the desired ImageCardView height and width and the application
context.

private static int CARD_WIDTH = 200;


private static int CARD_HEIGHT = 200;

private static Context mContext;

→ Implement the currently-empty abstract methods onCreateViewHolder, onBindViewHolder,


and onUnbindViewHolder in the CardPresenter class.

@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup) {
return null;
}
@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object o) {

}
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {

Create a Picasso Target to handle image loading


We're leveraging Picasso, an open source library that simplifies image loading, caching, and
resizing.

The base sample app already uses Picasso so this should be done for you. But if you want to add it
in a separate app, in your build.gradle file add the following dependency.

compile 'com.squareup.picasso:picasso:2.3.4'

→ Create an inner static


class PicassoImageCardViewTarget implementing com.squareup.picasso.Targetand implement the
methods: onBitmapLoaded, onBitmapFailed, and onPrepareLoad.
static class PicassoImageCardViewTarget implements Target {

@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {

@Override
public void onBitmapFailed(Drawable errorDrawable) {

@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {

}
}

→ Add a variable to store the ImageCardView we'll draw into once the bitmap is loaded.

private ImageCardView mImageCardView;

→ Create a constructor with the target ImageCardView as the parameter and store it as the
instance's mImageCardView.

public PicassoImageCardViewTarget(ImageCardView mImageCardView) {


this.mImageCardView = mImageCardView;
}

→ In onBitmapLoaded, create a new Drawable from the bitmap and set it as the main image for
the ImageCardView.

Drawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap);


mImageCardView.setMainImage(bitmapDrawable);

→ In onBitmapFailed, set the ImageCardView image to the error default.

mImageCardView.setMainImage(errorDrawable);

Create ViewHolder class


We'll use a ViewHolder to store all of the data associated with the view.

→ As a child of CardPresenter, create an inner static class that extends Presenter.ViewHolder and
create the default constructor.

static class ViewHolder extends Presenter.ViewHolder {

public ViewHolder(View view) {


super(view);
}
}
→ Define class variables to store the ImageCardView, Drawable, and PicassoImageCardViewTarget.

private ImageCardView mCardView;


private Drawable mDefaultCardImage;
private PicassoImageCardViewTarget mImageCardViewTarget;

→ In the constructor, cast the view parameter as an ImageCardView and store it in mCardView.
Instantiate a new PicassoImageCardViewTarget passing the cardView as the target parameter.
Finally, get the default card image from resources.

public ViewHolder(View view) {


super(view);
mCardView = (ImageCardView) view;
mImageCardViewTarget = new PicassoImageCardViewTarget(mCardView);
mDefaultCardImage = mContext
.getResources()
.getDrawable(R.drawable.filmi);
}

→ Add a getter for mCardView.

public ImageCardView getCardView() {


return mCardView;
}

→ Create a function that loads the image from a String URL.

protected void updateCardViewImage(String url) {

Picasso.with(mContext)
.load(url)
.resize(CARD_WIDTH * 2, CARD_HEIGHT * 2)
.centerCrop()
.error(mDefaultCardImage)
.into(mImageCardViewTarget);
}

Now let's create the ImageCardView and bind it with some data from the model.

Create the ImageCardView


onCreateViewHolder is called to create a new view. In it we'll handle the logic of storing the context,
and creating a new ImageCardView.

@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup) {

Log.d("onCreateViewHolder", "creating viewholder");


mContext = viewGroup.getContext();
ImageCardView cardView = new ImageCardView(mContext);
cardView.setFocusable(true);
cardView.setFocusableInTouchMode(true);
((TextView)cardView.findViewById(R.id.content_text)).setTextColor(Color.LTGRAY);
return new ViewHolder(cardView);
}

We set the cardView Focusable and FocusableInTouchMode to true to enable it to be selected when
browsing through the rows of content. It's important to remember to set these fields to true when
implementing Android TV for your app.

Finally we set the TextColor of the ImageCardView to light gray.

Bind data to the ViewHolder


We define the data binding logic in onBindViewHolder. We can cast the Object that's being passed in
as our Video data, then set the title text, subtext / content text, and image dimensions. Finally we tell
it to load the image with a thumbnail URL.

@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object o) {
Video video = (Video) o;
((ViewHolder) viewHolder).mCardView.setTitleText(video.getTitle());
((ViewHolder) viewHolder).mCardView.setContentText(video.getDescription());
((ViewHolder) viewHolder).mCardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT);
((ViewHolder) viewHolder).updateCardViewImage(video.getThumbUrl());
}

Make sure to include the Video class:

import com.android.example.leanback.data.Video;
And our CardPresenter is complete. Let's fill out some ListRows with our video content.

Populate the videos


In the LeanbackBrowseFragment let's create some sample categories. Here we're defining them as
constants, but in a real app you would probably pull them from your database.

private static final String[] HEADERS = new String[]{


"Featured", "Popular", "Editor's choice"
};

In init after we've set the badge drawable, we'll loop through the categories and create a row of
content for each one.

In each row, we'll create an ObjectAdapter to define how to render the content that we'll pull from our
database. We'll load the videos, create a header, and finally instantiate a ListRow with the header
and video data and add it to mRowsAdapter.

public void init() {


mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
setAdapter(mRowsAdapter);

setBrandColor(ContextCompat.getColor(getContext(), R.color.primary));
setBadgeDrawable(ContextCompat.getDrawable(getContext(), R.drawable.filmi));

for (int position = 0; position < HEADERS.length; position++) {


ObjectAdapter rowContents = new CursorObjectAdapter((new SinglePresenterSelector(new
CardPresenter())));
VideoDataManager manager = new VideoDataManager(getActivity(),
getLoaderManager(),
VideoItemContract.VideoItem.buildDirUri(),
rowContents);
manager.startDataLoading();

HeaderItem headerItem = new HeaderItem(position, HEADERS[position]);


mRowsAdapter.add(new ListRow(headerItem, manager.getItemList()));
}
}

Update VideoDataManager to use CursorObjectAdapter


Here, we'll update our VideoDataManager to manage the cursor for our ObjectAdapter.

In data/VideoDataManager add an ObjectAdapter to the class VideoDataManager.

private ObjectAdapter mItemList;

Next, add ObjectAdapter as fourth parameter to VideoDataManager constructor and store


the ObjectAdapteras mItemList.
public VideoDataManager(Context mContext, LoaderManager mLoaderManager, Uri mRowUri,
ObjectAdapter rowContents) {
mItemList = rowContents;
....
}

In VideoDataManager set the LOADER_ID to a random integer and replace the video instantiation
with setting the mapper for mItemList.

LOADER_ID = Double.valueOf(Math.random() * Integer.MAX_VALUE).intValue();


((CursorObjectAdapter)mItemList).setMapper(mMapper);

Create the a getter for the ObjectAdapter.

public ObjectAdapter getItemList() {


return mItemList;
}

Update VideoItemMapper to extend CursorMapper.

public static class VideoItemMapper extends CursorMapper {

Update onLoadFinished to set the cursor for mItemList.

public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {


if (mItemList instanceof CursorObjectAdapter) {
((CursorObjectAdapter) mItemList).swapCursor(cursor);
}
}

Fill in onLoaderReset to set the Cursor as null.

public void onLoaderReset(Loader<Cursor> cursorLoader) {


if (mItemList instanceof CursorObjectAdapter) {
((CursorObjectAdapter) mItemList).swapCursor(null);
}
}

Congrats! You've completed this step. Try running the app on Android TV.

You can modify the default Activity that's launched from Android Studio. Click Edit Configurations

Under Activity change the radio button to launch LeanbackActivity. You should see a screen similar to
the one below.
Summary
In this step you've learned about:

 The BrowseFragment and how you can populate it with your videos
 Tying your video data to the view through the MVP pattern

Next up
Let's create the video details activity.

4. Creating the video details activity


The Leanback library includes classes for displaying additional information about a media item, such
as a description or reviews, and for taking action on that item, such as purchasing it or playing its
content. This lesson discusses how to create a presenter class for media item details, and how to
extend the DetailsFragment class to implement a details view for a media item when it is selected by
a user.
In this step, you'll learn about:

 Showing details of a video in a dedicated Activity.


 Creating an action to play the video to start the video player.
 Displaying related videos in a row below the detail card.

DetailsFragment concepts
First let's cover some concepts about how the DetailsFragment works. It functions very similarly to
the BrowseFragment.

The classes controlling DetailsOverviewRow and Additional Row are defined in


a ClassPresenterSelectorallowing for flexibility.
The FullWidthDetailsOverviewRowPresenter contains the video image, information and available
actions. The Additional Row can be used to add related content or other controls.

FullWidthDetailsOverviewRowPresenter handles binding the video information to


the DetailsOverviewRow.

The Additional Row can be populated with a ListRow just like the BrowseFragment.

Alright, let's create a video detail view.

Create a new activity VideoDetailsActivity


First, create a new Blank Activity to handle the detail fragment.

→ Create an additional Activity VideoDetailsActivity under fastlane. Name the layout


file activity_leanback_details.

→ Just like when you created the LeanbackActivity, you'll need to delete the menu resources and
menu related methods and set it to extend Activity instead of ActionBarActivity.

→ Under layout, open activity_leanback_details. Replace the default layout with the following code.
Here, we're specifying that the Activity consists of a single fragment, VideoDetailsFragment.

<?xml version="1.0" encoding="utf-8"?>


<fragment xmlns:android="http://schemas.android.com/apk/res/android"
android:name="com.android.example.leanback.fastlane.VideoDetailsFragment"
android:id="@+id/details_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>

Don't worry about the errors, we'll work on creating the Fragment in an upcoming step.

Now that we have the activity framework, let's add the Leanback style to the activity declaration in
the manifest.

Update Activity theme in the manifest


We need to update the style of this Activity to Leanback. We can re-use the AppTheme we defined
previously which inherits from Leanback.

<activity android:name=".fastlane.VideoDetailsActivity"
android:label="@string/title_activity_player"
android:theme="@style/AppTheme"
android:exported="true">
</activity>

Now that the activity is styled correctly, we need to create the VideoDetailsFragment that we
reference in the layout.
Create the VideoDetailsFragment framework
Under fastlane create a new class called VideoDetailsFragment extending DetailsFragment.

We'll define a few class variables to store the Video information and some constants for image sizes
and action ids.

private Video selectedVideo;


private static final int DETAIL_THUMB_WIDTH = 274;
private static final int DETAIL_THUMB_HEIGHT = 274;
private static final int ACTION_PLAY = 1;
private static final int ACTION_WATCH_LATER = 2;

In VideoDetailsFragment, we need to do several things:

 Define the details presenter


 Load the movie thumbnail image
 Create a DetailsOverviewRow to display video details
 Create a presenter to bind the video data to the view
 Add a ListRow for recommended items
 Handle user actions

Let's start by getting the selected video from the intent. We'll override onCreate and get the video
from the intent.

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
selectedVideo = (Video) getActivity()
.getIntent()
.getSerializableExtra(Video.INTENT_EXTRA_VIDEO);
}

Create DetailsDescriptionPresenter
Before we get the image and create the DetailsOverviewRow we need to define a Presenter to bind
the data. The Leanback framework provides the AbstractDetailsDescriptionPresenter class for this
purpose, a nearly complete implementation of the presenter for media item details.

→ Under fastlane create a new


class DetailsDescriptionPresenter extending AbstractDetailsDescriptionPresenter.

→ Override onBindDescription. Cast the item as a Video object then get and set the Title, Subtitle and
Body.

@Override
protected void onBindDescription(ViewHolder viewHolder, Object o) {
Video video = (Video) o;
if (video != null) {
Log.d("Presenter", String.format("%s, %s, %s", video.getTitle(), video.getThumbUrl(),
video.getDescription()));
viewHolder.getTitle().setText(video.getTitle());
viewHolder.getSubtitle().setText(String.format(mContext.getString(R.string.rating), video.getRating()));
viewHolder.getBody().setText(video.getDescription());
}
}

Create AsyncTask to load the image


In order to not block the main UI thread, we create an AsyncTask to load the thumbnail bitmap.
In VideoDetailsFragment create a DetailsRowBuilderTask class
extending AsyncTask with Video, Integer, and DetailsOverviewRow as the parameter, progress, and
result respectively.

private class DetailRowBuilderTask extends AsyncTask<Video, Integer, DetailsOverviewRow> {


@Override
protected DetailsOverviewRow doInBackground(Video... videos) {
DetailsOverviewRow row = new DetailsOverviewRow(videos[0]);
Bitmap poster = null;
try {
// the Picasso library helps us dealing with images
poster = Picasso.with(getActivity())
.load(videos[0].getThumbUrl())
.resize(dpToPx(DETAIL_THUMB_WIDTH, getActivity().getApplicationContext()),
dpToPx(DETAIL_THUMB_HEIGHT, getActivity().getApplicationContext()))
.centerCrop()
.get();
} catch (IOException e) {
e.printStackTrace();
}
row.setImageBitmap(getActivity(), poster);

SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter();


adapter.set(ACTION_PLAY, new Action(ACTION_PLAY,
getResources().getString(R.string.action_play)));
adapter.set(ACTION_WATCH_LATER, new Action(ACTION_WATCH_LATER,
getResources().getString(R.string.action_watch_later)));
row.setActionsAdapter(adapter);

return row;
}
}

Here we're instantiating a new DetailsOverviewRow passing in the current Video as the main item for
the details page. We create a holder Bitmap variable to load the thumbnail. We use Picasso to load
and resize the image and store in poster.

Next we set poster as the bitmap.

Finally, we specify the actions by creating a SparseArrayObjectAdapter to hold our actions, and
setting this as the DetailsOverviewRow's action adapter. In this app, we
have ACTION_PLAY and ACTION_WATCH_LATER, but you could define a purchase or rent action.
In strings.xml define the text for action_play and action_watch_later.

<string name="action_play">PLAY</string>
<string name="action_watch_later">WATCH LATER</string>

To assist in calculating the appropriate screen size in DP, create a utility function.

public static int dpToPx(int dp, Context ctx) {


float density = ctx.getResources().getDisplayMetrics().density;
return Math.round((float) dp * density);
}

Now that the image has loaded, we can create the rest of the details fragment.

Creating a Presenter when the task has finished executing


The Picasso library loads and and resizes the image off the UI thread. After it has completed we
create the presenters in the onPostExecute method of the AsyncTask and set the adapter of
the DetailsFragment.

→ Override onPostExecute.

@Override
protected void onPostExecute(DetailsOverviewRow detailRow) {

→ Instantiate a new ClassPresenterSelector. This object allows you to define the presenters for each
portion of DetailFragment.

ClassPresenterSelector ps = new ClassPresenterSelector();

→ Instantiate a new FullWidthDetailsOverviewRowPresenter passing in a new Instance


of DetailsDescriptionPresenter as a parameter.

FullWidthDetailsOverviewRowPresenter detailsPresenter = new


FullWidthDetailsOverviewRowPresenter(new DetailsDescriptionPresenter(getContext()));

→ We can add a custom background color programmatically.

detailsPresenter.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.primary));
detailsPresenter.setInitialState(FullWidthDetailsOverviewRowPresenter.STATE_FULL);

→ Add an onActionClickedListener by creating a new OnActionClickedListener and


implementing onActionClicked. In onActionClicked, check the actionid. If the action
is ACTION_PLAY we want to Intent to the VideoPlayer Activity passing the video details. Otherwise,
we'll create a toast to display a Stringdefining the action.

detailsPresenter.setOnActionClickedListener(new OnActionClickedListener() {
@Override
public void onActionClicked(Action action) {
if (action.getId() == ACTION_PLAY) {
Intent intent = new Intent(getActivity(), PlayerActivity.class);
intent.putExtra(Video.INTENT_EXTRA_VIDEO, selectedVideo);
startActivity(intent);
} else {
Toast.makeText(getActivity(), action.toString(), Toast.LENGTH_SHORT).show();
}
}
});

→ Add the FullWidthDetailsOverviewRowPresenter to ClassPresenterSelector.

ps.addClassPresenter(DetailsOverviewRow.class, detailsPresenter);

→ Instantiate a new ArrayObjectAdapter passing in the ClassPresenterSelector. Then, add


the DetailRow to the ArrayObjectAdapter.

ArrayObjectAdapter adapter = new ArrayObjectAdapter(ps);


adapter.add(detailRow);

→ Finally, set the adapter.

setAdapter(adapter);

When we're done, the onPostExecute method should look like the following:

@Override
protected void onPostExecute(DetailsOverviewRow detailRow) {
ClassPresenterSelector ps = new ClassPresenterSelector();
FullWidthDetailsOverviewRowPresenter detailsPresenter = new
FullWidthDetailsOverviewRowPresenter(new DetailsDescriptionPresenter(getContext()));

// Add some style


detailsPresenter.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.primary));
detailsPresenter.setInitialState(FullWidthDetailsOverviewRowPresenter.STATE_FULL);
// we listen to two different actions: play and show
detailsPresenter.setOnActionClickedListener(new OnActionClickedListener() {
@Override
public void onActionClicked(Action action) {
if (action.getId() == ACTION_PLAY) {
Intent intent = new Intent(getActivity(), PlayerActivity.class);
intent.putExtra(Video.INTENT_EXTRA_VIDEO, selectedVideo);
startActivity(intent);
} else {
Toast.makeText(getActivity(), action.toString(), Toast.LENGTH_SHORT).show();
}
}
});

ps.addClassPresenter(DetailsOverviewRow.class, detailsPresenter);

ArrayObjectAdapter adapter = new ArrayObjectAdapter(ps);


adapter.add(detailRow);
// finally we set the adapter of the DetailsFragment
setAdapter(adapter);
}
Add a row of related videos below the detail panel
As an additional step, add a row of related videos below the detail card. To do this we only have to
add another presenter and adapter to the onPostExecute method of the AsyncTask

First, let's add an additional presenter:

ps.addClassPresenter(ListRow.class, new ListRowPresenter());

Accordingly, the ArrayObjectAdapter requires an additional ListRow to be added. We create a


new ListRow to which we pass a HeaderItem and a CursorObjectAdapter in the constructor just like
we did for the BrowseFragment.

String subcategories[] = {
"You may also like"
};

CursorObjectAdapter rowAdapter = new CursorObjectAdapter(


new SinglePresenterSelector(new CardPresenter()));
VideoDataManager manager = new VideoDataManager(getActivity(),getLoaderManager(),
VideoItemContract.VideoItem.buildDirUri(),rowAdapter);
manager.startDataLoading();
HeaderItem header = new HeaderItem(0, subcategories[0]);
adapter.add(new ListRow(header, rowAdapter));

Executing the builder task to build the view


Now we instantiate and execute the DetailRowBuilderTask in the onCreate method of
the VideoDetailsFragment.

new DetailRowBuilderTask().execute(selectedVideo);

Create Intent from the browse activity to the details activity


Now that we've completed the DetailsFragment, we need to modify the browse fragment to send an
intent to the details view when a user clicks on a media item. In order to enable this behavior, add
an OnItemViewClickedListener object to LeanbackBrowseFragment that fires an intent to
start VideoDetailsActivity.

In init set the onItemViewClickedListener. Here we're using a helper function to generate
the onItemViewClickedListener.

public class LeanbackBrowseFragment extends BrowseFragment {


...
public void init() {
...
setOnItemViewClickedListener(getDefaultItemViewClickedListener());
...
}
...
}
Create the helper function getDefaultItemViewClickerListener which returns a
new OnItemViewClickedListener.

private OnItemViewClickedListener getDefaultItemViewClickedListener() {


return new OnItemViewClickedListener() {
@Override
public void onItemClicked(Presenter.ViewHolder viewHolder, Object o,
RowPresenter.ViewHolder viewHolder2, Row row) {

Intent intent = new Intent(getActivity(), VideoDetailsActivity.class);


intent.putExtra(Video.INTENT_EXTRA_VIDEO, (Serializable)o);
startActivity(intent);
}
};
}

As we now pass the Video object with an Intent, we use a key Video.INTENT_EXTRA_VIDEO for it.

Congrats, you've finished this step! Compile, run and watch how you can play videos now!
Summary
In this step you've learned about:

 DetailsFragment
 Details Row Presenters
 How to add a recommendations row

Next up
Creating recommendations that display on the home screen.

5. Recommending content on the home screen


When interacting with TVs, users generally prefer to give minimal input before watching content. An
ideal scenario for many TV users is: sit down, turn on, and watch. The fewest steps to get users to
the content they enjoy is generally the path they prefer.

Content recommendations appear as the first row of the TV launch screen after the first use of the
device. Contributing recommendations from your app's content catalog can help bring users back to
your app.

In this step, you'll learn how to create recommendations and provide them to the Android framework
so your app content can be easily discovered and enjoyed by users.

In this step, you'll learn about:

 Creating a Recommendations Service


 Building Recommendations
 Running the Recommendations Service
Create a Recommendations Service
Content recommendations are created with background processing. In order for your application to
contribute to recommendations, create a service that periodically adds listings from your app's
catalog to the system list of recommendations.

→ Under fastlane create a new class RecommendationsService extending IntentService.

public class RecommendationsService extends IntentService

→ We'll define a few constants for rendering and tagging, and define a NotificationManager

private static final String TAG = "RecommendationsService";


private static final int MAX_RECOMMENDATIONS = 3;
public static final String EXTRA_BACKGROUND_IMAGE_URL = "background_image_url";
private static final int DETAIL_THUMB_WIDTH = 274;
private static final int DETAIL_THUMB_HEIGHT = 274;
private NotificationManager mNotificationManager;

→ Create the default constructor.

public RecommendationsService() {
super("RecommendationsService");
}

→ Next override the onHandleIntent function. As an example recommendation service, we'll use the
same video selections as the browse fragment. We store a ContentProviderClient, then create
a Cursor from the client.

@Override
protected void onHandleIntent(Intent intent) {
ContentProviderClient client = getContentResolver()
.acquireContentProviderClient(VideoItemContract.VideoItem.buildDirUri());
try {
Cursor cursor = client.query(VideoItemContract.VideoItem.buildDirUri(),
VideoDataManager.PROJECTION,
null,
null,
VideoItemContract.VideoItem.DEFAULT_SORT);

→ Instantiate a VideoItemMapper that we've defined in VideoDataManager and map it to cursor


with bindColumns.

VideoDataManager.VideoItemMapper mapper = new VideoDataManager.VideoItemMapper();


mapper.bindColumns(cursor);

→ Instantiate a NotificationManager.

mNotificationManager = (NotificationManager) getApplicationContext()


.getSystemService(Context.NOTIFICATION_SERVICE);

→ Create a counter for the iteration to create recommendations up to MAX_RECOMMENDATIONS.

int count = 1;

→ Loop through the cursor until we're out of recommendations or we've hit our max, and create
pending intents for each. The pending intents will direct the user to the details view of the video.

while (cursor.moveToNext() && count <= MAX_RECOMMENDATIONS) {


Video video = mapper.bind(cursor);
PendingIntent pendingIntent = buildPendingIntent(video);
Bundle extras = new Bundle();
extras.putString(EXTRA_BACKGROUND_IMAGE_URL, video.getThumbUrl());
count++;
}

→ Create a utility function to create the PendingIntent from Video.

private PendingIntent buildPendingIntent(Video video) {


Intent detailsIntent = new Intent(this, PlayerActivity.class);
detailsIntent.putExtra(Video.INTENT_EXTRA_VIDEO, video);

TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);


stackBuilder.addParentStack(VideoDetailsActivity.class);
stackBuilder.addNextIntent(detailsIntent);
// Ensure a unique PendingIntents, otherwise all recommendations end up with the same
// PendingIntent
detailsIntent.setAction(Long.toString(video.getId()));

PendingIntent intent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);


return intent;
}

→ Finally close the cursor and catch potential errors and you should have something similar to the
code below.
@Override
protected void onHandleIntent(Intent intent) {
ContentProviderClient client =
getContentResolver().acquireContentProviderClient(VideoItemContract.VideoItem.buildDirUri());
try {
Cursor cursor = client.query(VideoItemContract.VideoItem.buildDirUri(),
VideoDataManager.PROJECTION, null, null, VideoItemContract.VideoItem.DEFAULT_SORT);

VideoDataManager.VideoItemMapper mapper = new VideoDataManager.VideoItemMapper();


mapper.bindColumns(cursor);

mNotificationManager = (NotificationManager) getApplicationContext()


.getSystemService(Context.NOTIFICATION_SERVICE);

int count = 1;
while (cursor.moveToNext() && count <= MAX_RECOMMENDATIONS) {
Video video = mapper.bind(cursor);
PendingIntent pendingIntent = buildPendingIntent(video);
Bundle extras = new Bundle();
extras.putString(EXTRA_BACKGROUND_IMAGE_URL, video.getThumbUrl());
count++;
}
cursor.close();
} catch (RemoteException re) {

} catch (IOException re) {

} finally {
mNotificationManager = null;
}
}

Building Recommendations
Once the recommended videos are loaded, the service must create recommendations and pass
them to the Android framework. The framework receives the recommendations as Notification
objects that use a specific template and are marked with a specific category.

The following code example demonstrates how to get an instance of the NotificationManager, build a
recommendation, and pass it to the manager. This code needs to be added in the while loop after
the PendingIntenthas been created.

Bitmap image = Picasso.with(getApplicationContext())


.load(video.getThumbUrl())
.resize(VideoDetailsFragment.dpToPx(DETAIL_THUMB_WIDTH, getApplicationContext()),
VideoDetailsFragment.dpToPx(DETAIL_THUMB_WIDTH, getApplicationContext()))
.get();

Notification notification = new NotificationCompat.BigPictureStyle(


new NotificationCompat.Builder(getApplicationContext())
.setContentTitle(video.getTitle())
.setContentText(video.getDescription())
.setPriority(4)
.setLocalOnly(true)
.setOngoing(true)
.setColor(getApplicationContext().getResources().getColor(R.color.primary))
.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setLargeIcon(image)
.setSmallIcon(R.drawable.ic_stat_f)
.setContentIntent(pendingIntent)
.setExtras(extras))
.build();
mNotificationManager.notify(count, notification);

Add recommendation service to manifest


In order for this service to be recognized by the system and run, register it using your app manifest.
The following code snippet illustrates how to declare this class as a service:

<manifest ... >


<application ... >
...
<service android:name="com.android.example.leanback.fastlane.RecommendationsService"
android:enabled="true" android:exported="true"/>
</application>
</manifest>

Run the recommendations service


Your app's recommendation service must run periodically in order to create current
recommendations. To run your service, create a class that runs a timer and invokes it at regular
intervals. The following code example extends the BroadcastReceiver class to start periodic
execution of a recommendation service every 1/2 hour:

public class BootCompleteReceiver extends BroadcastReceiver {


private static final long INITIAL_DELAY = 5000;

public BootCompleteReceiver() {
}

@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().endsWith(Intent.ACTION_BOOT_COMPLETED)) {
scheduleRecommendationUpdate(context);
}
}
private void scheduleRecommendationUpdate(Context context) {

AlarmManager alarmManager = (AlarmManager)


context.getSystemService(Context.ALARM_SERVICE);
Intent recommendationIntent = new Intent(context, RecommendationsService.class);
PendingIntent alarmIntent = PendingIntent.getService(context, 0, recommendationIntent, 0);

alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
INITIAL_DELAY,
AlarmManager.INTERVAL_HALF_HOUR,
alarmIntent);
}
}

Add boot receiver to Android manifest


This implementation of the BroadcastReceiver class must run after startup of the TV device where it
is installed. To accomplish this, register this class in your app manifest with an intent filter that listens
for the completion of the device boot process. The following code demonstrates how to add this
configuration to the manifest.

<manifest ... >


<application ... >
<receiver android:name="com.android.example.leanback.fastlane.BootCompleteReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</application>
</manifest>

Important: Receiving a boot completed notification requires that your app requests
the RECEIVE_BOOT_COMPLETEDpermission. For more information,
see ACTION_BOOT_COMPLETED.

Congrats, you've completed adding recommendations for your app.

Try running it and you should start seeing recommendations after 30 minutes. If you want to see
something immediately, start the service through adb.

adb shell am startservice com.android.example.leanback/.fastlane.RecommendationsService

Summary
In this step you've learned about:

 Ideal user flow


 Creating and starting a recommendations service

Next up
Adding polish animations and transitions.

6. Polishing the animations


We want to specify the background image as a user browses from item to item in
the BrowseFragment
The Leanback library support developers in creating immersive TV experiences. This includes using
large pictures in the background to improve the experience. In this step you will learn how to use
the BackgroundManager to change the background of the user interface according to the selection of
the video in the UI. The BackgroundManager supports background image continuity between multiple
Activities. It should be noted that the BackgroundManager holds references to potentially large bitmap
Drawables. Call release to release these references when the Activity is not visible.

Concepts
 Using the BackgroundManager of the Leanback library.
 Encapsulating background management logic in a single class BackgroundHelper
 Using the Picasso library to load and manipulate bitmaps
 Implementing a Picasso Target

Create BackgroundHelper
Under fastlane create a class BackgroundHelper which we are going to extend step by step to add
functionality to change the background image for our TV activities.

public class BackgroundHelper {

private static long BACKGROUND_UPDATE_DELAY = 200;

private final Handler mHandler = new Handler();

private Activity mActivity;


private DisplayMetrics mMetrics;
private Timer mBackgroundTimer;
private String mBackgroundURL;

private Drawable mDefaultBackground;


private Target mBackgroundTarget;

public BackgroundHelper(Activity mActivity) {


this.mActivity = mActivity;
}

public void setBackgroundUrl(String backgroundUrl) {


this.mBackgroundURL = backgroundUrl;
}
}

Add an inner class PicassoBackgroundManagerTarget


The interface Target of the Picasso library acts as a listener for the end of loading and manipulating
images which is off the UI thread. We need a custom implementation of the Target that sets the
resulting bitmap as a background image. Note that is important to have a proper implementation
of hashCode and equals for every Targetimplementation.

static class PicassoBackgroundManagerTarget implements Target {


BackgroundManager mBackgroundManager;

public PicassoBackgroundManagerTarget(BackgroundManager backgroundManager) {


this.mBackgroundManager = backgroundManager;
}

@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) {
this.mBackgroundManager.setBitmap(bitmap);
}

@Override
public void onBitmapFailed(Drawable drawable) {
this.mBackgroundManager.setDrawable(drawable);
}

@Override
public void onPrepareLoad(Drawable drawable) {
// Do nothing, default_background manager has its own transitions
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}

PicassoBackgroundManagerTarget that = (PicassoBackgroundManagerTarget) o;

return mBackgroundManager.equals(that.mBackgroundManager);
}

@Override
public int hashCode() {
return mBackgroundManager.hashCode();
}
}

Create a method prepareBackgroundManager to instantiate and prepare the


PicassoBackgroundManagerTarget
We attach the Window of the current activity to the BackgroundManager and instantiate
the PicassoBackgroundManagerTarget. We set a default color for the background and get the metrics
that describe the size and density of this display.

public void prepareBackgroundManager() {


BackgroundManager backgroundManager = BackgroundManager.getInstance(mActivity);
backgroundManager.attach(mActivity.getWindow());

mBackgroundTarget = new PicassoBackgroundManagerTarget(backgroundManager);


mDefaultBackground = ContextCompat.getDrawable(mActivity, R.drawable.default_background);

mMetrics = new DisplayMetrics();


mActivity.getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
}

Add a method updateBackground to load an image


We are using Picasso to load and manipulate the image. Once done, the instance of
the PicassoBackgroundManagerTarget is used to apply the loaded image to the UI. The method also
makes sure to cancel the timer to make sure only one timer is running.

protected void updateBackground(String url) {


Picasso.with(mActivity)
.load(url)
.resize(mMetrics.widthPixels, mMetrics.heightPixels)
.centerCrop()
.transform(BlurTransform.getInstance(mActivity))
.error(mDefaultBackground)
.into(mBackgroundTarget);

if (null != mBackgroundTimer) {
mBackgroundTimer.cancel();
}
}

Add an inner class UpdateBackgroundTask


This is a subclass of TimerTask to be used to delay updating the background.

private class UpdateBackgroundTask extends TimerTask {


@Override
public void run() {
mHandler.post(new Runnable() {
@Override
public void run() {
if (mBackgroundURL != null) {
updateBackground(mBackgroundURL);
}
}
});
}
}

Create the methods startBackgroundTimer


In this method UpdateBackgroundTask is used to schedule a Timer to update the background.

public void startBackgroundTimer() {


if (null != mBackgroundTimer) {
mBackgroundTimer.cancel();
}
mBackgroundTimer = new Timer();
mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY);
}

Create the class BlurTransform


This is an implementation of the interface com.squareup.picasso.Transformation which we use to blur
the image to be set as background. We start with auto-generated dummy implementations of the
required methods transform and key.

public class BlurTransform implements Transformation {


@Override
public Bitmap transform(Bitmap source) {
return null;
}

@Override
public String key() {
return null;
}
}

Make BlurTransformation a singleton


We want the BlurTransformation to exist only once, so we make it a Singleton and instantiate
a RenderScript in the private constructor which takes a Context as single argument.

RenderScript rs;

static BlurTransform blurTransform;

protected BlurTransform() {
// Exists only to defeat instantiation.
}

private BlurTransform(Context context) {


super();
rs = RenderScript.create(context);
}

public static BlurTransform getInstance(Context context) {


if (blurTransform == null) {
blurTransform = new BlurTransform(context);
}
return blurTransform;
}

Implement the transform and key method


The meat of this class is in the transform method which does the trick of blurring the image.

@Override
public Bitmap transform(Bitmap bitmap) {
// Create another bitmap that will hold the results of the filter.
Bitmap blurredBitmap = Bitmap.createBitmap(bitmap);

// Allocate memory for Renderscript to work with


Allocation input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_FULL,
Allocation.USAGE_SHARED);
Allocation output = Allocation.createTyped(rs, input.getType());

// Load up an instance of the specific script that we want to use.


ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
script.setInput(input);

// Set the blur radius


script.setRadius(20);

// Start the ScriptIntrinsicBlur


script.forEach(output);

// Copy the output to the blurred bitmap


output.copyTo(blurredBitmap);

bitmap.recycle();

return blurredBitmap;
}

@Override
public String key() {
return "blur";
}

Apply the transformation


We complete the calls to the Picasso library in the updateBackground method after
the centerCrop call.

.transform(BlurTransform.getInstance(mActivity))

Add background helper to LeanbackBrowseFragment


These classes are not used anywhere by now. We add it first to the LeanbackBrowseFragment. Add a
member variable bgHelper.

private BackgroundHelper bgHelper;

and instantiate it at the end of the init method of the LeanbackBrowseFragment:

bgHelper = new BackgroundHelper(getActivity());


bgHelper.prepareBackgroundManager();

The BackgroundHelper should be updated each time the user selects an item view. Create a factory
method getDefaultSelectedListener which does that:
protected OnItemViewSelectedListener getDefaultItemSelectedListener() {
return new OnItemViewSelectedListener() {
public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
RowPresenter.ViewHolder rowViewHolder, Row row) {
if (item instanceof Video) {
bgHelper.setBackgroundUrl(((Video) item).getThumbUrl());
bgHelper.startBackgroundTimer();
}

}
};
}

Now we just register the listener in the init method.

setOnItemViewSelectedListener(getDefaultItemSelectedListener());

Add background helper to the VideoDetailsFragment


The background should also be set when the details of a video are shown. So we apply this in
the VideoDetailsFragment as well. It's simpler here because we don't require a listener. It's just added
to the onCreate() method of the fragment.

Add the member variable:

BackgroundHelper bgHelper;

and use it in the onCreate method.

bgHelper = new BackgroundHelper(getActivity());


bgHelper.prepareBackgroundManager();
bgHelper.updateBackground(selectedVideo.getThumbUrl());

Run the app


Congrats, you've completed the codelab!

We have some bonus content on leveraging the PlaybackOverlayFragment to easily add playback
controls. The example for adding a PlaybackOverlayFragment to an existing player and detecting
appropriate playback controls can be found in checkpoint_6. The PlaybackOverlayFragment also
allows you to help users find related content in your app without stopping playback.

If you're looking for sample code for additional features, check out our full sample. It includes sample
code for the search fragment, grid fragment and others.

Das könnte Ihnen auch gefallen