public class

PlaybackBannerControlGlue<T extends PlayerAdapter>

extends PlaybackBaseControlGlue<PlayerAdapter>

 java.lang.Object

androidx.leanback.media.PlaybackGlue

androidx.leanback.media.PlaybackBaseControlGlue<PlayerAdapter>

↳androidx.leanback.media.PlaybackBannerControlGlue<T>

Gradle dependencies

compile group: 'androidx.leanback', name: 'leanback', version: '1.2.0-alpha02'

  • groupId: androidx.leanback
  • artifactId: leanback
  • version: 1.2.0-alpha02

Artifact androidx.leanback:leanback:1.2.0-alpha02 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.leanback:leanback com.android.support:leanback-v17

Androidx class mapping:

androidx.leanback.media.PlaybackBannerControlGlue android.support.v17.leanback.media.PlaybackBannerControlGlue

Overview

A helper class for managing a PlaybackControlsRow being displayed in PlaybackGlueHost. It supports standard playback control actions play/pause and skip next/previous. This helper class is a glue layer that manages interaction between the leanback UI components PlaybackControlsRow PlaybackControlsRowPresenter and a functional PlayerAdapter which represents the underlying media player.

Apps must pass a PlayerAdapter in the constructor for a specific implementation e.g. a MediaPlayerAdapter.

The glue has two action bars: primary action bars and secondary action bars. Apps can provide additional actions by overriding PlaybackBannerControlGlue.onCreatePrimaryActions(ArrayObjectAdapter) and / or PlaybackBaseControlGlue.onCreateSecondaryActions(ArrayObjectAdapter) and respond to actions by overriding PlaybackBannerControlGlue.onActionClicked(Action).

The subclass is responsible for implementing the "repeat mode" in PlaybackBannerControlGlue.onPlayCompleted().

Sample Code:

 public class MyVideoFragment extends VideoFragment {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         PlaybackBannerControlGlue playerGlue =
                 new PlaybackBannerControlGlue(getActivity(),
                         new MediaPlayerAdapter(getActivity()));
         playerGlue.setHost(new VideoFragmentGlueHost(this));
         playerGlue.setSubtitle("Leanback artist");
         playerGlue.setTitle("Leanback team at work");
         String uriPath = "android.resource://com.example.android.leanback/raw/video";
         playerGlue.getPlayerAdapter().setDataSource(Uri.parse(uriPath));
         playerGlue.playWhenPrepared();
     }
 }
 

Summary

Fields
public static final intACTION_CUSTOM_LEFT_FIRST

The adapter key for the first custom control on the left side of the predefined primary controls.

public static final intACTION_CUSTOM_RIGHT_FIRST

The adapter key for the first custom control on the right side of the predefined primary controls.

public static final intACTION_FAST_FORWARD

The adapter key for the fast forward control.

public static final intACTION_PLAY_PAUSE

The adapter key for the play/pause control.

public static final intACTION_REWIND

The adapter key for the rewind control.

public static final intACTION_SKIP_TO_NEXT

The adapter key for the skip to next control.

public static final intACTION_SKIP_TO_PREVIOUS

The adapter key for the skip to previous control.

public static final intPLAYBACK_SPEED_FAST_L0

The initial (level 0) fast forward playback speed.

public static final intPLAYBACK_SPEED_FAST_L1

The level 1 fast forward playback speed.

public static final intPLAYBACK_SPEED_FAST_L2

The level 2 fast forward playback speed.

public static final intPLAYBACK_SPEED_FAST_L3

The level 3 fast forward playback speed.

public static final intPLAYBACK_SPEED_FAST_L4

The level 4 fast forward playback speed.

public static final intPLAYBACK_SPEED_INVALID

Invalid playback speed.

public static final intPLAYBACK_SPEED_NORMAL

Speed representing playback state that is playing normally.

public static final intPLAYBACK_SPEED_PAUSED

Speed representing playback state that is paused.

from PlaybackBaseControlGlue<T>ACTION_REPEAT, ACTION_SHUFFLE
Constructors
publicPlaybackBannerControlGlue(Context context, int[] fastForwardSpeeds[], int[] rewindSpeeds[], PlayerAdapter impl)

Constructor for the glue.

publicPlaybackBannerControlGlue(Context context, int[] seekSpeeds[], PlayerAdapter impl)

Constructor for the glue.

Methods
public longgetCurrentPosition()

Gets current position of the player.

public int[]getFastForwardSpeeds()

Returns the fast forward speeds.

public int[]getRewindSpeeds()

Returns the rewind speeds.

public voidonActionClicked(Action action)

Handles action clicks.

protected voidonCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter)

May be overridden to add primary actions to the adapter.

protected PlaybackRowPresenteronCreateRowPresenter()

public booleanonKey(View v, int keyCode, KeyEvent event)

Handles key events and returns true if handled.

protected voidonPlayCompleted()

Event when play finishes, subclass may handling repeat mode here.

protected voidonPlayStateChanged()

Event when play state changed.

public voidpause()

Pauses the media player.

public voidplay()

Starts the media player.

public voidsetControlsRow(PlaybackControlsRow controlsRow)

Sets the controls row to be managed by the glue layer.

from PlaybackBaseControlGlue<T>getArt, getBufferedPosition, getControlsRow, getDuration, getPlaybackRowPresenter, getPlayerAdapter, getSubtitle, getSupportedActions, getTitle, isControlsOverlayAutoHideEnabled, isPlaying, isPrepared, next, notifyItemChanged, onAttachedToHost, onCreateSecondaryActions, onDetachedFromHost, onHostStart, onHostStop, onMetadataChanged, onPreparedStateChanged, onUpdateBufferedProgress, onUpdateDuration, onUpdateProgress, previous, seekTo, setArt, setControlsOverlayAutoHideEnabled, setPlaybackRowPresenter, setSubtitle, setTitle
from PlaybackGlueaddPlayerCallback, getContext, getHost, getPlayerCallbacks, onHostPause, onHostResume, playWhenPrepared, removePlayerCallback, setHost
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final int ACTION_CUSTOM_LEFT_FIRST

The adapter key for the first custom control on the left side of the predefined primary controls.

public static final int ACTION_SKIP_TO_PREVIOUS

The adapter key for the skip to previous control.

public static final int ACTION_REWIND

The adapter key for the rewind control.

public static final int ACTION_PLAY_PAUSE

The adapter key for the play/pause control.

public static final int ACTION_FAST_FORWARD

The adapter key for the fast forward control.

public static final int ACTION_SKIP_TO_NEXT

The adapter key for the skip to next control.

public static final int ACTION_CUSTOM_RIGHT_FIRST

The adapter key for the first custom control on the right side of the predefined primary controls.

public static final int PLAYBACK_SPEED_INVALID

Invalid playback speed.

public static final int PLAYBACK_SPEED_PAUSED

Speed representing playback state that is paused.

public static final int PLAYBACK_SPEED_NORMAL

Speed representing playback state that is playing normally.

public static final int PLAYBACK_SPEED_FAST_L0

The initial (level 0) fast forward playback speed. The negative of this value is for rewind at the same speed.

public static final int PLAYBACK_SPEED_FAST_L1

The level 1 fast forward playback speed. The negative of this value is for rewind at the same speed.

public static final int PLAYBACK_SPEED_FAST_L2

The level 2 fast forward playback speed. The negative of this value is for rewind at the same speed.

public static final int PLAYBACK_SPEED_FAST_L3

The level 3 fast forward playback speed. The negative of this value is for rewind at the same speed.

public static final int PLAYBACK_SPEED_FAST_L4

The level 4 fast forward playback speed. The negative of this value is for rewind at the same speed.

Constructors

public PlaybackBannerControlGlue(Context context, int[] seekSpeeds[], PlayerAdapter impl)

Constructor for the glue.

Parameters:

context:
seekSpeeds: The array of seek speeds for fast forward and rewind. The maximum length of the array is defined as NUMBER_OF_SEEK_SPEEDS.
impl: Implementation to underlying media player.

public PlaybackBannerControlGlue(Context context, int[] fastForwardSpeeds[], int[] rewindSpeeds[], PlayerAdapter impl)

Constructor for the glue.

Parameters:

context:
fastForwardSpeeds: The array of seek speeds for fast forward. The maximum length of the array is defined as NUMBER_OF_SEEK_SPEEDS.
rewindSpeeds: The array of seek speeds for rewind. The maximum length of the array is defined as NUMBER_OF_SEEK_SPEEDS.
impl: Implementation to underlying media player.

Methods

public void setControlsRow(PlaybackControlsRow controlsRow)

Sets the controls row to be managed by the glue layer. If PlaybackControlsRow.getPrimaryActionsAdapter() is not provided, a default ArrayObjectAdapter will be created and initialized in PlaybackBaseControlGlue.onCreatePrimaryActions(ArrayObjectAdapter). If PlaybackControlsRow.getSecondaryActionsAdapter() is not provided, a default ArrayObjectAdapter will be created and initialized in PlaybackBaseControlGlue.onCreateSecondaryActions(ArrayObjectAdapter). The primary actions and playback state related aspects of the row are updated by the glue.

protected void onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter)

May be overridden to add primary actions to the adapter. Default implementation add .

Parameters:

primaryActionsAdapter: The adapter to add primary Actions.

protected PlaybackRowPresenter onCreateRowPresenter()

public void onActionClicked(Action action)

Handles action clicks. A subclass may override this add support for additional actions.

public boolean onKey(View v, int keyCode, KeyEvent event)

Handles key events and returns true if handled. A subclass may override this to provide additional support.

protected void onPlayStateChanged()

Event when play state changed.

protected void onPlayCompleted()

Event when play finishes, subclass may handling repeat mode here.

public int[] getFastForwardSpeeds()

Returns the fast forward speeds.

public int[] getRewindSpeeds()

Returns the rewind speeds.

public long getCurrentPosition()

Gets current position of the player. If the player is playing/paused, this method returns current position from PlayerAdapter. Otherwise, if the player is fastforwarding/rewinding, the method fake-pauses the PlayerAdapter and returns its own calculated position.

Returns:

Current position of the player.

public void play()

Starts the media player. Does nothing if PlaybackGlue.isPrepared() is false. To wait PlaybackGlue.isPrepared() to be true before playing, use PlaybackGlue.playWhenPrepared().

public void pause()

Pauses the media player.

Source

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.leanback.media;

import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;

import android.content.Context;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.leanback.widget.AbstractDetailsDescriptionPresenter;
import androidx.leanback.widget.Action;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ObjectAdapter;
import androidx.leanback.widget.PlaybackControlsRow;
import androidx.leanback.widget.PlaybackControlsRowPresenter;
import androidx.leanback.widget.PlaybackRowPresenter;
import androidx.leanback.widget.RowPresenter;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * A helper class for managing a {@link PlaybackControlsRow} being displayed in
 * {@link PlaybackGlueHost}. It supports standard playback control actions play/pause and
 * skip next/previous. This helper class is a glue layer that manages interaction between the
 * leanback UI components {@link PlaybackControlsRow} {@link PlaybackControlsRowPresenter}
 * and a functional {@link PlayerAdapter} which represents the underlying
 * media player.
 *
 * <p>Apps must pass a {@link PlayerAdapter} in the constructor for a specific
 * implementation e.g. a {@link MediaPlayerAdapter}.
 * </p>
 *
 * <p>The glue has two action bars: primary action bars and secondary action bars. Apps
 * can provide additional actions by overriding {@link #onCreatePrimaryActions} and / or
 * {@link #onCreateSecondaryActions} and respond to actions by overriding
 * {@link #onActionClicked(Action)}.
 * </p>
 *
 * <p>The subclass is responsible for implementing the "repeat mode" in
 * {@link #onPlayCompleted()}.
 * </p>
 *
 * Sample Code:
 * <pre><code>
 * public class MyVideoFragment extends VideoFragment {
 *     &#64;Override
 *     public void onCreate(Bundle savedInstanceState) {
 *         super.onCreate(savedInstanceState);
 *         PlaybackBannerControlGlue<MediaPlayerAdapter> playerGlue =
 *                 new PlaybackBannerControlGlue(getActivity(),
 *                         new MediaPlayerAdapter(getActivity()));
 *         playerGlue.setHost(new VideoFragmentGlueHost(this));
 *         playerGlue.setSubtitle("Leanback artist");
 *         playerGlue.setTitle("Leanback team at work");
 *         String uriPath = "android.resource://com.example.android.leanback/raw/video";
 *         playerGlue.getPlayerAdapter().setDataSource(Uri.parse(uriPath));
 *         playerGlue.playWhenPrepared();
 *     }
 * }
 * </code></pre>
 * @param <T> Type of {@link PlayerAdapter} passed in constructor.
 */
public class PlaybackBannerControlGlue<T extends PlayerAdapter>
        extends PlaybackBaseControlGlue<T> {

    /** @hide */
    @IntDef(
            flag = true,
            value = {
            ACTION_CUSTOM_LEFT_FIRST,
            ACTION_SKIP_TO_PREVIOUS,
            ACTION_REWIND,
            ACTION_PLAY_PAUSE,
            ACTION_FAST_FORWARD,
            ACTION_SKIP_TO_NEXT,
            ACTION_CUSTOM_RIGHT_FIRST
    })
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    @Retention(RetentionPolicy.SOURCE)
    public @interface ACTION_ {}

    /**
     * The adapter key for the first custom control on the left side
     * of the predefined primary controls.
     */
    public static final int ACTION_CUSTOM_LEFT_FIRST =
            PlaybackBaseControlGlue.ACTION_CUSTOM_LEFT_FIRST;

    /**
     * The adapter key for the skip to previous control.
     */
    public static final int ACTION_SKIP_TO_PREVIOUS =
            PlaybackBaseControlGlue.ACTION_SKIP_TO_PREVIOUS;

    /**
     * The adapter key for the rewind control.
     */
    public static final int ACTION_REWIND = PlaybackBaseControlGlue.ACTION_REWIND;

    /**
     * The adapter key for the play/pause control.
     */
    public static final int ACTION_PLAY_PAUSE = PlaybackBaseControlGlue.ACTION_PLAY_PAUSE;

    /**
     * The adapter key for the fast forward control.
     */
    public static final int ACTION_FAST_FORWARD = PlaybackBaseControlGlue.ACTION_FAST_FORWARD;

    /**
     * The adapter key for the skip to next control.
     */
    public static final int ACTION_SKIP_TO_NEXT = PlaybackBaseControlGlue.ACTION_SKIP_TO_NEXT;

    /**
     * The adapter key for the first custom control on the right side
     * of the predefined primary controls.
     */
    public static final int ACTION_CUSTOM_RIGHT_FIRST =
            PlaybackBaseControlGlue.ACTION_CUSTOM_RIGHT_FIRST;


    /** @hide */
    @IntDef({
                    PLAYBACK_SPEED_INVALID,
                    PLAYBACK_SPEED_PAUSED,
                    PLAYBACK_SPEED_NORMAL,
                    PLAYBACK_SPEED_FAST_L0,
                    PLAYBACK_SPEED_FAST_L1,
                    PLAYBACK_SPEED_FAST_L2,
                    PLAYBACK_SPEED_FAST_L3,
                    PLAYBACK_SPEED_FAST_L4
    })
    @RestrictTo(LIBRARY_GROUP_PREFIX)
    @Retention(RetentionPolicy.SOURCE)
    private @interface SPEED {}

    /**
     * Invalid playback speed.
     */
    public static final int PLAYBACK_SPEED_INVALID = -1;

    /**
     * Speed representing playback state that is paused.
     */
    public static final int PLAYBACK_SPEED_PAUSED = 0;

    /**
     * Speed representing playback state that is playing normally.
     */
    public static final int PLAYBACK_SPEED_NORMAL = 1;

    /**
     * The initial (level 0) fast forward playback speed.
     * The negative of this value is for rewind at the same speed.
     */
    public static final int PLAYBACK_SPEED_FAST_L0 = 10;

    /**
     * The level 1 fast forward playback speed.
     * The negative of this value is for rewind at the same speed.
     */
    public static final int PLAYBACK_SPEED_FAST_L1 = 11;

    /**
     * The level 2 fast forward playback speed.
     * The negative of this value is for rewind at the same speed.
     */
    public static final int PLAYBACK_SPEED_FAST_L2 = 12;

    /**
     * The level 3 fast forward playback speed.
     * The negative of this value is for rewind at the same speed.
     */
    public static final int PLAYBACK_SPEED_FAST_L3 = 13;

    /**
     * The level 4 fast forward playback speed.
     * The negative of this value is for rewind at the same speed.
     */
    public static final int PLAYBACK_SPEED_FAST_L4 = 14;

    private static final String TAG = PlaybackBannerControlGlue.class.getSimpleName();
    private static final int NUMBER_OF_SEEK_SPEEDS = PLAYBACK_SPEED_FAST_L4
            - PLAYBACK_SPEED_FAST_L0 + 1;

    private final int[] mFastForwardSpeeds;
    private final int[] mRewindSpeeds;
    private PlaybackControlsRow.SkipNextAction mSkipNextAction;
    private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction;
    private PlaybackControlsRow.FastForwardAction mFastForwardAction;
    private PlaybackControlsRow.RewindAction mRewindAction;

    @SPEED
    private int mPlaybackSpeed = PLAYBACK_SPEED_PAUSED;
    private long mStartTime;
    private long mStartPosition = 0;

    // Flag for is customized FastForward/ Rewind Action supported.
    // If customized actions are not supported, the adapter can still use default behavior through
    // setting ACTION_REWIND and ACTION_FAST_FORWARD as supported actions.
    private boolean mIsCustomizedFastForwardSupported;
    private boolean mIsCustomizedRewindSupported;

    /**
     * Constructor for the glue.
     *
     * @param context
     * @param seekSpeeds The array of seek speeds for fast forward and rewind. The maximum length of
     *                   the array is defined as NUMBER_OF_SEEK_SPEEDS.
     * @param impl Implementation to underlying media player.
     */
    public PlaybackBannerControlGlue(Context context,
            int[] seekSpeeds,
            T impl) {
        this(context, seekSpeeds, seekSpeeds, impl);
    }

    /**
     * Constructor for the glue.
     *
     * @param context
     * @param fastForwardSpeeds The array of seek speeds for fast forward. The maximum length of
     *                   the array is defined as NUMBER_OF_SEEK_SPEEDS.
     * @param rewindSpeeds The array of seek speeds for rewind. The maximum length of
     *                   the array is defined as NUMBER_OF_SEEK_SPEEDS.
     * @param impl Implementation to underlying media player.
     */
    public PlaybackBannerControlGlue(Context context,
                                    int[] fastForwardSpeeds,
                                    int[] rewindSpeeds,
                                    T impl) {
        super(context, impl);

        if (fastForwardSpeeds.length == 0 || fastForwardSpeeds.length > NUMBER_OF_SEEK_SPEEDS) {
            throw new IllegalArgumentException("invalid fastForwardSpeeds array size");
        }
        mFastForwardSpeeds = fastForwardSpeeds;

        if (rewindSpeeds.length == 0 || rewindSpeeds.length > NUMBER_OF_SEEK_SPEEDS) {
            throw new IllegalArgumentException("invalid rewindSpeeds array size");
        }
        mRewindSpeeds = rewindSpeeds;
        if ((mPlayerAdapter.getSupportedActions() & ACTION_FAST_FORWARD) != 0) {
            mIsCustomizedFastForwardSupported = true;
        }
        if ((mPlayerAdapter.getSupportedActions() & ACTION_REWIND) != 0) {
            mIsCustomizedRewindSupported = true;
        }
    }

    @Override
    public void setControlsRow(PlaybackControlsRow controlsRow) {
        super.setControlsRow(controlsRow);
        onUpdatePlaybackState();
    }

    @Override
    protected void onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter) {
        final long supportedActions = getSupportedActions();
        if ((supportedActions & ACTION_SKIP_TO_PREVIOUS) != 0 && mSkipPreviousAction == null) {
            primaryActionsAdapter.add(mSkipPreviousAction =
                    new PlaybackControlsRow.SkipPreviousAction(getContext()));
        } else if ((supportedActions & ACTION_SKIP_TO_PREVIOUS) == 0
                && mSkipPreviousAction != null) {
            primaryActionsAdapter.remove(mSkipPreviousAction);
            mSkipPreviousAction = null;
        }
        if ((supportedActions & ACTION_REWIND) != 0 && mRewindAction == null) {
            primaryActionsAdapter.add(mRewindAction =
                    new PlaybackControlsRow.RewindAction(getContext(), mRewindSpeeds.length));
        } else if ((supportedActions & ACTION_REWIND) == 0 && mRewindAction != null) {
            primaryActionsAdapter.remove(mRewindAction);
            mRewindAction = null;
        }
        if ((supportedActions & ACTION_PLAY_PAUSE) != 0 && mPlayPauseAction == null) {
            mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(getContext());
            primaryActionsAdapter.add(mPlayPauseAction =
                    new PlaybackControlsRow.PlayPauseAction(getContext()));
        } else if ((supportedActions & ACTION_PLAY_PAUSE) == 0 && mPlayPauseAction != null) {
            primaryActionsAdapter.remove(mPlayPauseAction);
            mPlayPauseAction = null;
        }
        if ((supportedActions & ACTION_FAST_FORWARD) != 0 && mFastForwardAction == null) {
            mFastForwardAction = new PlaybackControlsRow.FastForwardAction(getContext(),
                    mFastForwardSpeeds.length);
            primaryActionsAdapter.add(mFastForwardAction =
                    new PlaybackControlsRow.FastForwardAction(getContext(),
                            mFastForwardSpeeds.length));
        } else if ((supportedActions & ACTION_FAST_FORWARD) == 0 && mFastForwardAction != null) {
            primaryActionsAdapter.remove(mFastForwardAction);
            mFastForwardAction = null;
        }
        if ((supportedActions & ACTION_SKIP_TO_NEXT) != 0 && mSkipNextAction == null) {
            primaryActionsAdapter.add(mSkipNextAction =
                    new PlaybackControlsRow.SkipNextAction(getContext()));
        } else if ((supportedActions & ACTION_SKIP_TO_NEXT) == 0 && mSkipNextAction != null) {
            primaryActionsAdapter.remove(mSkipNextAction);
            mSkipNextAction = null;
        }
    }

    @Override
    protected PlaybackRowPresenter onCreateRowPresenter() {
        final AbstractDetailsDescriptionPresenter detailsPresenter =
                new AbstractDetailsDescriptionPresenter() {
                    @Override
                    protected void onBindDescription(ViewHolder
                            viewHolder, Object object) {
                        PlaybackBannerControlGlue glue = (PlaybackBannerControlGlue) object;
                        viewHolder.getTitle().setText(glue.getTitle());
                        viewHolder.getSubtitle().setText(glue.getSubtitle());
                    }
                };

        PlaybackControlsRowPresenter rowPresenter =
                new PlaybackControlsRowPresenter(detailsPresenter) {
            @Override
            protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
                super.onBindRowViewHolder(vh, item);
                vh.setOnKeyListener(PlaybackBannerControlGlue.this);
            }
            @Override
            protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
                super.onUnbindRowViewHolder(vh);
                vh.setOnKeyListener(null);
            }
        };

        return rowPresenter;
    }

    /**
     * Handles action clicks.  A subclass may override this add support for additional actions.
     */
    @Override
    public void onActionClicked(Action action) {
        dispatchAction(action, null);
    }

    /**
     * Handles key events and returns true if handled.  A subclass may override this to provide
     * additional support.
     */
    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_UP:
            case KeyEvent.KEYCODE_DPAD_DOWN:
            case KeyEvent.KEYCODE_DPAD_RIGHT:
            case KeyEvent.KEYCODE_DPAD_LEFT:
            case KeyEvent.KEYCODE_BACK:
            case KeyEvent.KEYCODE_ESCAPE:
                boolean abortSeek = mPlaybackSpeed >= PLAYBACK_SPEED_FAST_L0
                        || mPlaybackSpeed <= -PLAYBACK_SPEED_FAST_L0;
                if (abortSeek) {
                    play();
                    onUpdatePlaybackStatusAfterUserAction();
                    return keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE;
                }
                return false;
        }

        final ObjectAdapter primaryActionsAdapter = mControlsRow.getPrimaryActionsAdapter();
        Action action = mControlsRow.getActionForKeyCode(primaryActionsAdapter, keyCode);
        if (action == null) {
            action = mControlsRow.getActionForKeyCode(mControlsRow.getSecondaryActionsAdapter(),
                    keyCode);
        }

        if (action != null) {
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                dispatchAction(action, event);
            }
            return true;
        }
        return false;
    }

    void onUpdatePlaybackStatusAfterUserAction() {
        updatePlaybackState(mIsPlaying);
    }

    // Helper function to increment mPlaybackSpeed when necessary. The mPlaybackSpeed will control
    // the UI of fast forward button in control row.
    private void incrementFastForwardPlaybackSpeed() {
        switch (mPlaybackSpeed) {
            case PLAYBACK_SPEED_FAST_L0:
            case PLAYBACK_SPEED_FAST_L1:
            case PLAYBACK_SPEED_FAST_L2:
            case PLAYBACK_SPEED_FAST_L3:
                mPlaybackSpeed++;
                break;
            default:
                mPlaybackSpeed = PLAYBACK_SPEED_FAST_L0;
                break;
        }
    }

    // Helper function to decrement mPlaybackSpeed when necessary. The mPlaybackSpeed will control
    // the UI of rewind button in control row.
    private void decrementRewindPlaybackSpeed() {
        switch (mPlaybackSpeed) {
            case -PLAYBACK_SPEED_FAST_L0:
            case -PLAYBACK_SPEED_FAST_L1:
            case -PLAYBACK_SPEED_FAST_L2:
            case -PLAYBACK_SPEED_FAST_L3:
                mPlaybackSpeed--;
                break;
            default:
                mPlaybackSpeed = -PLAYBACK_SPEED_FAST_L0;
                break;
        }
    }

    /**
     * Called when the given action is invoked, either by click or key event.
     */
    boolean dispatchAction(Action action, KeyEvent keyEvent) {
        boolean handled = false;
        if (action == mPlayPauseAction) {
            boolean canPlay = keyEvent == null
                    || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
                    || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY;
            boolean canPause = keyEvent == null
                    || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
                    || keyEvent.getKeyCode() == KeyEvent.KEYCODE_MEDIA_PAUSE;
            //            PLAY_PAUSE    PLAY      PAUSE
            // playing    paused                  paused
            // paused     playing       playing
            // ff/rw      playing       playing   paused
            if (canPause
                    && (canPlay ? mPlaybackSpeed == PLAYBACK_SPEED_NORMAL :
                    mPlaybackSpeed != PLAYBACK_SPEED_PAUSED)) {
                pause();
            } else if (canPlay && mPlaybackSpeed != PLAYBACK_SPEED_NORMAL) {
                play();
            }
            onUpdatePlaybackStatusAfterUserAction();
            handled = true;
        } else if (action == mSkipNextAction) {
            next();
            handled = true;
        } else if (action == mSkipPreviousAction) {
            previous();
            handled = true;
        } else if (action == mFastForwardAction) {
            if (mPlayerAdapter.isPrepared() && mPlaybackSpeed < getMaxForwardSpeedId()) {
                // When the customized fast forward action is available, it will be executed
                // when fast forward button is pressed. If current media item is not playing, the UI
                // will be updated to PLAYING status.
                if (mIsCustomizedFastForwardSupported) {
                    // Change UI to Playing status.
                    mIsPlaying = true;
                    // Execute customized fast forward action.
                    mPlayerAdapter.fastForward();
                } else {
                    // When the customized fast forward action is not supported, the fakePause
                    // operation is needed to stop the media item but still indicating the media
                    // item is playing from the UI perspective
                    // Also the fakePause() method must be called before
                    // incrementFastForwardPlaybackSpeed() method to make sure fake fast forward
                    // computation is accurate.
                    fakePause();
                }
                // Change mPlaybackSpeed to control the UI.
                incrementFastForwardPlaybackSpeed();
                onUpdatePlaybackStatusAfterUserAction();
            }
            handled = true;
        } else if (action == mRewindAction) {
            if (mPlayerAdapter.isPrepared() && mPlaybackSpeed > -getMaxRewindSpeedId()) {
                if (mIsCustomizedFastForwardSupported) {
                    mIsPlaying = true;
                    mPlayerAdapter.rewind();
                } else {
                    fakePause();
                }
                decrementRewindPlaybackSpeed();
                onUpdatePlaybackStatusAfterUserAction();
            }
            handled = true;
        }
        return handled;
    }

    @Override
    protected void onPlayStateChanged() {
        if (DEBUG) Log.v(TAG, "onStateChanged");

        onUpdatePlaybackState();
        super.onPlayStateChanged();
    }

    @Override
    protected void onPlayCompleted() {
        super.onPlayCompleted();
        mIsPlaying = false;
        mPlaybackSpeed = PLAYBACK_SPEED_PAUSED;
        mStartPosition = getCurrentPosition();
        mStartTime = System.currentTimeMillis();
        onUpdatePlaybackState();
    }

    void onUpdatePlaybackState() {
        updatePlaybackState(mIsPlaying);
    }

    private void updatePlaybackState(boolean isPlaying) {
        if (mControlsRow == null) {
            return;
        }

        if (!isPlaying) {
            onUpdateProgress();
            mPlayerAdapter.setProgressUpdatingEnabled(false);
        } else {
            mPlayerAdapter.setProgressUpdatingEnabled(true);
        }

        if (mFadeWhenPlaying && getHost() != null) {
            getHost().setControlsOverlayAutoHideEnabled(isPlaying);
        }


        final ArrayObjectAdapter primaryActionsAdapter =
                (ArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
        if (mPlayPauseAction != null) {
            int index = !isPlaying
                    ? PlaybackControlsRow.PlayPauseAction.INDEX_PLAY
                    : PlaybackControlsRow.PlayPauseAction.INDEX_PAUSE;
            if (mPlayPauseAction.getIndex() != index) {
                mPlayPauseAction.setIndex(index);
                notifyItemChanged(primaryActionsAdapter, mPlayPauseAction);
            }
        }

        if (mFastForwardAction != null) {
            int index = 0;
            if (mPlaybackSpeed >= PLAYBACK_SPEED_FAST_L0) {
                index = mPlaybackSpeed - PLAYBACK_SPEED_FAST_L0 + 1;
            }
            if (mFastForwardAction.getIndex() != index) {
                mFastForwardAction.setIndex(index);
                notifyItemChanged(primaryActionsAdapter, mFastForwardAction);
            }
        }
        if (mRewindAction != null) {
            int index = 0;
            if (mPlaybackSpeed <= -PLAYBACK_SPEED_FAST_L0) {
                index = -mPlaybackSpeed - PLAYBACK_SPEED_FAST_L0 + 1;
            }
            if (mRewindAction.getIndex() != index) {
                mRewindAction.setIndex(index);
                notifyItemChanged(primaryActionsAdapter, mRewindAction);
            }
        }
    }

    /**
     * Returns the fast forward speeds.
     */
    @NonNull
    public int[] getFastForwardSpeeds() {
        return mFastForwardSpeeds;
    }

    /**
     * Returns the rewind speeds.
     */
    @NonNull
    public int[] getRewindSpeeds() {
        return mRewindSpeeds;
    }

    private int getMaxForwardSpeedId() {
        return PLAYBACK_SPEED_FAST_L0 + (mFastForwardSpeeds.length - 1);
    }

    private int getMaxRewindSpeedId() {
        return PLAYBACK_SPEED_FAST_L0 + (mRewindSpeeds.length - 1);
    }

    /**
     * Gets current position of the player. If the player is playing/paused, this
     * method returns current position from {@link PlayerAdapter}. Otherwise, if the player is
     * fastforwarding/rewinding, the method fake-pauses the {@link PlayerAdapter} and returns its
     * own calculated position.
     * @return Current position of the player.
     */
    @Override
    public long getCurrentPosition() {
        int speed;
        if (mPlaybackSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED
                || mPlaybackSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) {
            // If the adapter is playing/paused, using the position from adapter instead.
            return mPlayerAdapter.getCurrentPosition();
        } else if (mPlaybackSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
            // If fast forward operation is supported in this scenario, current player position
            // can be get from mPlayerAdapter.getCurrentPosition() directly
            if (mIsCustomizedFastForwardSupported) {
                return mPlayerAdapter.getCurrentPosition();
            }
            int index = mPlaybackSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
            speed = getFastForwardSpeeds()[index];
        } else if (mPlaybackSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
            // If fast rewind is supported in this scenario, current player position
            // can be get from mPlayerAdapter.getCurrentPosition() directly
            if (mIsCustomizedRewindSupported) {
                return mPlayerAdapter.getCurrentPosition();
            }
            int index = -mPlaybackSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
            speed = -getRewindSpeeds()[index];
        } else {
            return -1;
        }

        long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed;
        if (position > getDuration()) {
            mPlaybackSpeed = PLAYBACK_SPEED_PAUSED;
            position = getDuration();
            mPlayerAdapter.seekTo(position);
            mStartPosition = 0;
            pause();
        } else if (position < 0) {
            mPlaybackSpeed = PLAYBACK_SPEED_PAUSED;
            position = 0;
            mPlayerAdapter.seekTo(position);
            mStartPosition = 0;
            pause();
        }
        return position;
    }


    @Override
    public void play() {
        if (!mPlayerAdapter.isPrepared()) {
            return;
        }

        // Solves the situation that a player pause at the end and click play button. At this case
        // the player will restart from the beginning.
        if (mPlaybackSpeed == PLAYBACK_SPEED_PAUSED
                && mPlayerAdapter.getCurrentPosition() >= mPlayerAdapter.getDuration()) {
            mStartPosition = 0;
        } else {
            mStartPosition = getCurrentPosition();
        }

        mStartTime = System.currentTimeMillis();
        mIsPlaying = true;
        mPlaybackSpeed = PLAYBACK_SPEED_NORMAL;
        mPlayerAdapter.seekTo(mStartPosition);
        super.play();

        onUpdatePlaybackState();
    }

    @Override
    public void pause() {
        mIsPlaying = false;
        mPlaybackSpeed = PLAYBACK_SPEED_PAUSED;
        mStartPosition = getCurrentPosition();
        mStartTime = System.currentTimeMillis();
        super.pause();

        onUpdatePlaybackState();
    }

    /**
     * Control row shows PLAY, but the media is actually paused when the player is
     * fastforwarding/rewinding.
     */
    private void fakePause() {
        mIsPlaying = true;
        mStartPosition = getCurrentPosition();
        mStartTime = System.currentTimeMillis();
        super.pause();

        onUpdatePlaybackState();
    }
}