Android TV – How to add leanback media controls to Google ExoPlayer
by Riley MacDonald, September 21, 2017

This post demonstrates how to add (Android) leanback controls to ExoPlayer to control playback. I couldn’t find any tutorials outlining the implementation details of leanback controls. The amount of classes and their dependencies resulted in a sluggish implementation.

Here’s an outline of what’s covered in this post:

  • Adding the leanback library dependency
  • Adding a view to house the leanback controls
  • Customizing the leanback controls
  • Initializing the classes required
  • Displaying the controls

Classes Used
Here’s a list of the classes used in this post:

Add the leanback dependency:
Add the leanback library by adding it to your modules build.gradle. At the time of this writing I used 24.1.1 because I have an minSdk of 16. Use of this library may require the v4 support library depending on your applications setup.

compile "com.android.support:leanback-v17:24.1.1"

Add a new view to hold the leanback controls:
This view should be a sibling of the same parent view of your ExoPlayer view. In this example the video_player layout has a width and height of match_parent.

1
2
3
4
5
6
7
8
9
10
11
12
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
 
    <include android:id="@id/video_player" layout="@layout/my_video_player" />
 
    <FrameLayout
        android:id="@+id/my_playback_overlay_fragment_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</merge>

Create a subclass of PlaybackOverlayFragment
Add a setupLeanbackControls() to the onCreate() method to hold the code below.

1
2
3
4
5
6
7
8
public class MyPlaybackOverlayFragment extends PlaybackOverlayFragment {
    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        setupLeanbackControls();
    }
}

Create a PlaybackControlsRowPresenter:
The PlaybackControlsRowPresenter is used to display a series of playback control buttons. If you don’t want your secondary actions (beyond rewind, play/pause, and fast forward) to be hidden behind a “more actions” button use setSecondaryActionsHidden(false).

1
2
PlaybackControlsRowPresenter playbackControlsRowPresenter = new PlaybackControlsRowPresenter();
playbackControlsRowPresenter.setSecondaryActionsHidden(false);

Create a ControlButtonPresenterSelector:
The ControlButtonPresenterSelector is used to display primary and secondary controls for PlaybackControlsRow (see below). Add the ControlButtonPresenterSelector to a ArrayObjectAdapter (see below).

1
2
final ControlButtonPresenterSelector controlButtonPresenterSelector = new ControlButtonPresenterSelector();
ArrayObjectAdapter primaryActionsAdapter = new ArrayObjectAdapter(controlButtonPresenterSelector);

Create primary leanback actions:
These represent the buttons which will appear on the leanback controls when shown. Add the desired primary controls to the primaryActionsAdapter created above.

1
2
3
4
5
6
7
playPauseAction = new PlayPauseAction(getActivity());
rewindAction = new RewindAction(getActivity());
fastForwardAction = new FastForwardAction(getActivity());
 
primaryActionsAdapter.add(rewindAction);
primaryActionsAdapter.add(playPauseAction);
primaryActionsAdapter.add(fastForwardAction);

Create a ClassPresenterSelector:
The ClassPresenterSelector selects a Presenter based on the item’s Java class.

1
2
final ClassPresenterSelector classPresenterSelector = new ClassPresenterSelector();
classPresenterSelector.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter);

Finally, create a PlaybackControlsRow:
A Row of playback controls to be displayed by a PlaybackControlsRowPresenter. This row consists of some optional item detail, a series of primary actions, and an optional series of secondary actions.

1
2
3
4
5
PlaybackControlsRow playbackControlsRow = new PlaybackControlsRow();
ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(classPresenterSelector);
rowsAdapter.add(playbackControlsRow);
 
playbackControlsRow.setPrimaryActionsAdapter(primaryActionsAdapter);

Set the PlaybackOverlayFragment adapter:
This sets the list of rows for the fragment. This is a method of PlaybackOverlayFragment.

1
setAdapter(rowsAdapter);

All your MyPlaybackOverlayFragment.setupLeanbackControls() method should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void setupLeanbackControls() {
    PlaybackControlsRowPresenter playbackControlsRowPresenter = new PlaybackControlsRowPresenter();
    playbackControlsRowPresenter.setSecondaryActionsHidden(false);
 
    final ControlButtonPresenterSelector controlButtonPresenterSelector = new ControlButtonPresenterSelector();
    ArrayObjectAdapter primaryActionsAdapter = new ArrayObjectAdapter(controlButtonPresenterSelector);
 
    playPauseAction = new PlayPauseAction(getActivity());
    rewindAction = new RewindAction(getActivity());
    fastForwardAction = new FastForwardAction(getActivity());
 
    primaryActionsAdapter.add(rewindAction);
    primaryActionsAdapter.add(playPauseAction);
    primaryActionsAdapter.add(fastForwardAction);
 
    final ClassPresenterSelector classPresenterSelector = new ClassPresenterSelector();
    classPresenterSelector.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter);
 
    PlaybackControlsRow playbackControlsRow = new PlaybackControlsRow();
    ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(classPresenterSelector);
    rowsAdapter.add(playbackControlsRow);
 
    playbackControlsRow.setPrimaryActionsAdapter(primaryActionsAdapter);
 
    setAdapter(rowsAdapter);
}

Implement Action Listeners
You can set a OnActionClickedListener to the actions created / added in the example above (playPauseAction, rewindAction and fastForwardAction).

1
2
3
4
5
6
7
8
9
10
11
12
13
playbackControlsRowPresenter.setOnActionClickedListener(action -> {
    if (!isVisible()) return;
 
    if (action == playPauseAction) {
        setPlayWhenReady();
    } else if (action == fastForwardAction) {
        seek(DEFAULT_SEEK_LENGTH);
    } else if (action == rewindAction) {
        seek(-DEFAULT_SEEK_LENGTH);
    } else if (action == closedCaptioningAction) {
        setClosedCaptions();
    }
});

Populate the view we added earlier:
Now the controls are ready, we can add the video when starting ExoPlayer playback. I’ve omitted the details of ExoPlayer here for simplicity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TVPlayerActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        setupExoPlayer(); // Configure ExoPlayer, start video playback, etc.
 
        setupMyPlaybackOverlayFragment(); // Initialize the leanback controls
    }
 
    private void setupMyPlaybackOverlayFragment() {
        final MyPlaybackOverlayFragment myPlaybackOverlayFragment = new MyPlaybackOverlayFragment();
 
        final FragmentManager fragmentManager = getFragmentManager();
        fragmentManager.beginTransaction()
                .replace(R.id.my_playback_overlay_fragment_view, myPlaybackOverlayFragment, null)
                .commit();
    }
}

Temporarily Disable TV Screen Saver
Many Android TV devices have a built in screen saver that displays itself on top of video playback. To work around this I added a flag to the window during onCreate() by invoking.

getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

And then removed it during onStop() by invoking:

getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

Summary
The PlaybackOverlayFragment handles remote control input to show and hide (tickle()) the leanback controls. It somewhat simplifies implementing TV style controls for an existing player / application.

Open the comment form

Leave a comment:

Comments will be reviewed before they are posted.

User Comments:

Be the first to leave a comment on this post!