public abstract class

PlaybackBaseControlGlue<T extends PlayerAdapter>

extends PlaybackGlue

implements OnActionClickedListener

 java.lang.Object

androidx.leanback.media.PlaybackGlue

↳androidx.leanback.media.PlaybackBaseControlGlue<T>

Subclasses:

PlaybackTransportControlGlue<T>, PlaybackBannerControlGlue<T>

Gradle dependencies

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

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

Artifact androidx.leanback:leanback:1.2.0-alpha04 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.PlaybackBaseControlGlue android.support.v17.leanback.media.PlaybackBaseControlGlue

Overview

A base abstract 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 PlaybackRowPresenter and a functional PlayerAdapter which represents the underlying media player.

The app must pass a PlayerAdapter in 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 PlaybackBaseControlGlue.onCreatePrimaryActions(ArrayObjectAdapter) and / or PlaybackBaseControlGlue.onCreateSecondaryActions(ArrayObjectAdapter) and respond to actions by overriding PlaybackBaseControlGlue.onActionClicked(Action).

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

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_REPEAT

The adapter key for the repeat control.

public static final intACTION_REWIND

The adapter key for the rewind control.

public static final intACTION_SHUFFLE

The adapter key for the shuffle 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.

Constructors
publicPlaybackBaseControlGlue(Context context, PlayerAdapter impl)

Constructor for the glue.

Methods
public DrawablegetArt()

public final longgetBufferedPosition()

public PlaybackControlsRowgetControlsRow()

Returns the playback controls row managed by the glue layer.

public longgetCurrentPosition()

public final longgetDuration()

public PlaybackRowPresentergetPlaybackRowPresenter()

Returns the playback controls row Presenter managed by the glue layer.

public final PlayerAdaptergetPlayerAdapter()

public java.lang.CharSequencegetSubtitle()

Return The media subtitle.

public longgetSupportedActions()

Returns a bitmask of actions supported by the media player.

public java.lang.CharSequencegetTitle()

Returns the title of the media item.

public booleanisControlsOverlayAutoHideEnabled()

Returns true if the controls auto hides after a timeout when media is playing.

public booleanisPlaying()

Returns true if media is currently playing.

public booleanisPrepared()

Returns true when the media player is prepared to start media playback.

public voidnext()

Goes to the next media item.

protected static voidnotifyItemChanged(ArrayObjectAdapter adapter, java.lang.Object object)

public abstract voidonActionClicked(Action action)

Handles action clicks.

protected voidonAttachedToHost(PlaybackGlueHost host)

This method is called attached to associated PlaybackGlueHost.

protected voidonCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter)

May be overridden to add primary actions to the adapter.

protected abstract PlaybackRowPresenteronCreateRowPresenter()

protected voidonCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter)

May be overridden to add secondary actions to the adapter.

protected voidonDetachedFromHost()

This method is called when current associated PlaybackGlueHost is attached to a different PlaybackGlue or PlaybackGlueHost is destroyed .

protected voidonHostStart()

This method is called when PlaybackGlueHost is started.

protected voidonHostStop()

This method is called when PlaybackGlueHost is stopped.

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

Handles key events and returns true if handled.

protected voidonMetadataChanged()

Event when metadata changed

protected voidonPlayCompleted()

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

protected voidonPlayStateChanged()

Event when play state changed.

protected voidonPreparedStateChanged()

Event when ready state for play changes.

protected voidonUpdateBufferedProgress()

protected voidonUpdateDuration()

protected voidonUpdateProgress()

public voidpause()

Pauses the media player.

public voidplay()

Starts the media player.

public voidprevious()

Goes to the previous media item.

public final voidseekTo(long position)

Seek media to a new position.

public voidsetArt(Drawable cover)

Sets the drawable representing cover image.

public voidsetControlsOverlayAutoHideEnabled(boolean enable)

Sets the controls to auto hide after a timeout when media is playing.

public voidsetControlsRow(PlaybackControlsRow controlsRow)

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

public voidsetPlaybackRowPresenter(PlaybackRowPresenter presenter)

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

public voidsetSubtitle(java.lang.CharSequence subtitle)

Sets the media subtitle.

public voidsetTitle(java.lang.CharSequence title)

Sets the media title.

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_REPEAT

The adapter key for the repeat control.

public static final int ACTION_SHUFFLE

The adapter key for the shuffle 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.

Constructors

public PlaybackBaseControlGlue(Context context, PlayerAdapter impl)

Constructor for the glue.

Parameters:

context:
impl: Implementation to underlying media player.

Methods

public final PlayerAdapter getPlayerAdapter()

protected void onAttachedToHost(PlaybackGlueHost host)

This method is called attached to associated PlaybackGlueHost. Subclass may override and call super.onAttachedToHost().

protected void onHostStart()

This method is called when PlaybackGlueHost is started. Subclass may override.

protected void onHostStop()

This method is called when PlaybackGlueHost is stopped. Subclass may override.

protected void onDetachedFromHost()

This method is called when current associated PlaybackGlueHost is attached to a different PlaybackGlue or PlaybackGlueHost is destroyed . Subclass may override and call super.onDetachedFromHost() at last. A typical PlaybackGlue will release resources (e.g. MediaPlayer or connection to playback service) in this method.

protected abstract PlaybackRowPresenter onCreateRowPresenter()

public void setControlsOverlayAutoHideEnabled(boolean enable)

Sets the controls to auto hide after a timeout when media is playing.

Parameters:

enable: True to enable auto hide after a timeout when media is playing.

See also: PlaybackGlueHost.setControlsOverlayAutoHideEnabled(boolean)

public boolean isControlsOverlayAutoHideEnabled()

Returns true if the controls auto hides after a timeout when media is playing.

See also: PlaybackGlueHost.isControlsOverlayAutoHideEnabled()

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.

public void setPlaybackRowPresenter(PlaybackRowPresenter presenter)

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

public PlaybackControlsRow getControlsRow()

Returns the playback controls row managed by the glue layer.

public PlaybackRowPresenter getPlaybackRowPresenter()

Returns the playback controls row Presenter managed by the glue layer.

public abstract void onActionClicked(Action action)

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

public abstract 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.

public boolean isPlaying()

Returns true if media is currently playing.

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.

public void next()

Goes to the next media item. This method is optional.

public void previous()

Goes to the previous media item. This method is optional.

protected static void notifyItemChanged(ArrayObjectAdapter adapter, java.lang.Object object)

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 void onCreateSecondaryActions(ArrayObjectAdapter secondaryActionsAdapter)

May be overridden to add secondary actions to the adapter.

Parameters:

secondaryActionsAdapter: The adapter you need to add the Actions to.

protected void onUpdateProgress()

protected void onUpdateBufferedProgress()

protected void onUpdateDuration()

public final long getDuration()

Returns:

The duration of the media item in milliseconds.

public long getCurrentPosition()

Returns:

The current position of the media item in milliseconds.

public final long getBufferedPosition()

Returns:

The current buffered position of the media item in milliseconds.

public boolean isPrepared()

Returns true when the media player is prepared to start media playback. When returning false, app may listen to PlaybackGlue.PlayerCallback.onPreparedStateChanged(PlaybackGlue) event.

Returns:

True if prepared, false otherwise.

protected void onPreparedStateChanged()

Event when ready state for play changes.

public void setArt(Drawable cover)

Sets the drawable representing cover image. The drawable will be rendered by default description presenter in PlaybackTransportRowPresenter.setDescriptionPresenter(Presenter).

Parameters:

cover: The drawable representing cover image.

public Drawable getArt()

Returns:

The drawable representing cover image.

public void setSubtitle(java.lang.CharSequence subtitle)

Sets the media subtitle. The subtitle will be rendered by default description presenter PlaybackTransportRowPresenter.setDescriptionPresenter(Presenter).

Parameters:

subtitle: Subtitle to set.

public java.lang.CharSequence getSubtitle()

Return The media subtitle.

public void setTitle(java.lang.CharSequence title)

Sets the media title. The title will be rendered by default description presenter PlaybackTransportRowPresenter.setDescriptionPresenter(Presenter).

public java.lang.CharSequence getTitle()

Returns the title of the media item.

protected void onMetadataChanged()

Event when metadata changed

protected void onPlayStateChanged()

Event when play state changed.

protected void onPlayCompleted()

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

public final void seekTo(long position)

Seek media to a new position.

Parameters:

position: New position.

public long getSupportedActions()

Returns a bitmask of actions supported by 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 android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;

import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.leanback.widget.Action;
import androidx.leanback.widget.ArrayObjectAdapter;
import androidx.leanback.widget.ControlButtonPresenterSelector;
import androidx.leanback.widget.OnActionClickedListener;
import androidx.leanback.widget.PlaybackControlsRow;
import androidx.leanback.widget.PlaybackRowPresenter;
import androidx.leanback.widget.PlaybackTransportRowPresenter;
import androidx.leanback.widget.Presenter;

import java.util.List;

/**
 * A base abstract 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 PlaybackRowPresenter}
 * and a functional {@link PlayerAdapter} which represents the underlying
 * media player.
 *
 * <p>The app must pass a {@link PlayerAdapter} in 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>
 *
 * @param <T> Type of {@link PlayerAdapter} passed in constructor.
 */
public abstract class PlaybackBaseControlGlue<T extends PlayerAdapter> extends PlaybackGlue
        implements OnActionClickedListener, View.OnKeyListener {

    /**
     * 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 = 0x1;

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

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

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

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

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

    /**
     * The adapter key for the repeat control.
     */
    public static final int ACTION_REPEAT = 0x200;

    /**
     * The adapter key for the shuffle control.
     */
    public static final int ACTION_SHUFFLE = 0x400;

    /**
     * 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 = 0x1000;

    static final String TAG = "PlaybackTransportGlue";
    static final boolean DEBUG = false;

    final T mPlayerAdapter;
    PlaybackControlsRow mControlsRow;
    PlaybackRowPresenter mControlsRowPresenter;
    PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
    boolean mIsPlaying = false;
    boolean mFadeWhenPlaying = true;

    CharSequence mSubtitle;
    CharSequence mTitle;
    Drawable mCover;

    PlaybackGlueHost.PlayerCallback mPlayerCallback;
    boolean mBuffering = false;
    int mVideoWidth = 0;
    int mVideoHeight = 0;
    boolean mErrorSet = false;
    int mErrorCode;
    String mErrorMessage;

    final PlayerAdapter.Callback mAdapterCallback = new PlayerAdapter
            .Callback() {

        @Override
        public void onPlayStateChanged(@NonNull PlayerAdapter wrapper) {
            if (DEBUG) Log.v(TAG, "onPlayStateChanged");
            PlaybackBaseControlGlue.this.onPlayStateChanged();
        }

        @Override
        public void onCurrentPositionChanged(@NonNull PlayerAdapter wrapper) {
            if (DEBUG) Log.v(TAG, "onCurrentPositionChanged");
            PlaybackBaseControlGlue.this.onUpdateProgress();
        }

        @Override
        public void onBufferedPositionChanged(@NonNull PlayerAdapter wrapper) {
            if (DEBUG) Log.v(TAG, "onBufferedPositionChanged");
            PlaybackBaseControlGlue.this.onUpdateBufferedProgress();
        }

        @Override
        public void onDurationChanged(@NonNull PlayerAdapter wrapper) {
            if (DEBUG) Log.v(TAG, "onDurationChanged");
            PlaybackBaseControlGlue.this.onUpdateDuration();
        }

        @Override
        public void onPlayCompleted(@NonNull PlayerAdapter wrapper) {
            if (DEBUG) Log.v(TAG, "onPlayCompleted");
            PlaybackBaseControlGlue.this.onPlayCompleted();
        }

        @Override
        public void onPreparedStateChanged(@NonNull PlayerAdapter wrapper) {
            if (DEBUG) Log.v(TAG, "onPreparedStateChanged");
            PlaybackBaseControlGlue.this.onPreparedStateChanged();
        }

        @Override
        public void onVideoSizeChanged(@NonNull PlayerAdapter wrapper, int width, int height) {
            mVideoWidth = width;
            mVideoHeight = height;
            if (mPlayerCallback != null) {
                mPlayerCallback.onVideoSizeChanged(width, height);
            }
        }

        @Override
        public void onError(
                @NonNull PlayerAdapter wrapper,
                int errorCode,
                @Nullable String errorMessage
        ) {
            mErrorSet = true;
            mErrorCode = errorCode;
            mErrorMessage = errorMessage;
            if (mPlayerCallback != null) {
                mPlayerCallback.onError(errorCode, errorMessage);
            }
        }

        @Override
        public void onBufferingStateChanged(@NonNull PlayerAdapter wrapper, boolean start) {
            mBuffering = start;
            if (mPlayerCallback != null) {
                mPlayerCallback.onBufferingStateChanged(start);
            }
        }

        @Override
        public void onMetadataChanged(@NonNull PlayerAdapter wrapper) {
            PlaybackBaseControlGlue.this.onMetadataChanged();
        }
    };

    /**
     * Constructor for the glue.
     *
     * @param context
     * @param impl Implementation to underlying media player.
     */
    public PlaybackBaseControlGlue(@NonNull Context context, T impl) {
        super(context);
        mPlayerAdapter = impl;
        mPlayerAdapter.setCallback(mAdapterCallback);
    }

    public final T getPlayerAdapter() {
        return mPlayerAdapter;
    }

    @Override
    protected void onAttachedToHost(@NonNull PlaybackGlueHost host) {
        super.onAttachedToHost(host);
        host.setOnKeyInterceptListener(this);
        host.setOnActionClickedListener(this);
        onCreateDefaultControlsRow();
        onCreateDefaultRowPresenter();
        host.setPlaybackRowPresenter(getPlaybackRowPresenter());
        host.setPlaybackRow(getControlsRow());

        mPlayerCallback = host.getPlayerCallback();
        onAttachHostCallback();
        mPlayerAdapter.onAttachedToHost(host);
    }

    void onAttachHostCallback() {
        if (mPlayerCallback != null) {
            if (mVideoWidth != 0 && mVideoHeight != 0) {
                mPlayerCallback.onVideoSizeChanged(mVideoWidth, mVideoHeight);
            }
            if (mErrorSet) {
                mPlayerCallback.onError(mErrorCode, mErrorMessage);
            }
            mPlayerCallback.onBufferingStateChanged(mBuffering);
        }
    }

    void onDetachHostCallback() {
        mErrorSet = false;
        mErrorCode = 0;
        mErrorMessage = null;
        if (mPlayerCallback != null) {
            mPlayerCallback.onBufferingStateChanged(false);
        }
    }

    @Override
    protected void onHostStart() {
        mPlayerAdapter.setProgressUpdatingEnabled(true);
    }

    @Override
    protected void onHostStop() {
        mPlayerAdapter.setProgressUpdatingEnabled(false);
    }

    @Override
    protected void onDetachedFromHost() {
        onDetachHostCallback();
        mPlayerCallback = null;
        mPlayerAdapter.onDetachedFromHost();
        mPlayerAdapter.setProgressUpdatingEnabled(false);
        super.onDetachedFromHost();
    }

    void onCreateDefaultControlsRow() {
        if (mControlsRow == null) {
            PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
            setControlsRow(controlsRow);
        }
    }

    void onCreateDefaultRowPresenter() {
        if (mControlsRowPresenter == null) {
            setPlaybackRowPresenter(onCreateRowPresenter());
        }
    }

    @NonNull
    protected abstract PlaybackRowPresenter onCreateRowPresenter();

    /**
     * Sets the controls to auto hide after a timeout when media is playing.
     * @param enable True to enable auto hide after a timeout when media is playing.
     * @see PlaybackGlueHost#setControlsOverlayAutoHideEnabled(boolean)
     */
    public void setControlsOverlayAutoHideEnabled(boolean enable) {
        mFadeWhenPlaying = enable;
        if (!mFadeWhenPlaying && getHost() != null) {
            getHost().setControlsOverlayAutoHideEnabled(false);
        }
    }

    /**
     * Returns true if the controls auto hides after a timeout when media is playing.
     * @see PlaybackGlueHost#isControlsOverlayAutoHideEnabled()
     */
    public boolean isControlsOverlayAutoHideEnabled() {
        return mFadeWhenPlaying;
    }

    /**
     * Sets the controls row to be managed by the glue layer. If
     * {@link PlaybackControlsRow#getPrimaryActionsAdapter()} is not provided, a default
     * {@link ArrayObjectAdapter} will be created and initialized in
     * {@link #onCreatePrimaryActions(ArrayObjectAdapter)}. If
     * {@link PlaybackControlsRow#getSecondaryActionsAdapter()} is not provided, a default
     * {@link ArrayObjectAdapter} will be created and initialized in
     * {@link #onCreateSecondaryActions(ArrayObjectAdapter)}.
     * The primary actions and playback state related aspects of the row
     * are updated by the glue.
     */
    public void setControlsRow(@NonNull PlaybackControlsRow controlsRow) {
        mControlsRow = controlsRow;
        mControlsRow.setCurrentPosition(-1);
        mControlsRow.setDuration(-1);
        mControlsRow.setBufferedPosition(-1);
        if (mControlsRow.getPrimaryActionsAdapter() == null) {
            ArrayObjectAdapter adapter = new ArrayObjectAdapter(
                    new ControlButtonPresenterSelector());
            onCreatePrimaryActions(adapter);
            mControlsRow.setPrimaryActionsAdapter(adapter);
        }
        // Add secondary actions
        if (mControlsRow.getSecondaryActionsAdapter() == null) {
            ArrayObjectAdapter secondaryActions = new ArrayObjectAdapter(
                    new ControlButtonPresenterSelector());
            onCreateSecondaryActions(secondaryActions);
            getControlsRow().setSecondaryActionsAdapter(secondaryActions);
        }
        updateControlsRow();
    }

    /**
     * Sets the controls row Presenter to be managed by the glue layer.
     */
    public void setPlaybackRowPresenter(@Nullable PlaybackRowPresenter presenter) {
        mControlsRowPresenter = presenter;
    }

    /**
     * Returns the playback controls row managed by the glue layer.
     */
    @Nullable
    public PlaybackControlsRow getControlsRow() {
        return mControlsRow;
    }

    /**
     * Returns the playback controls row Presenter managed by the glue layer.
     */
    @Nullable
    public PlaybackRowPresenter getPlaybackRowPresenter() {
        return mControlsRowPresenter;
    }

    /**
     * Handles action clicks.  A subclass may override this add support for additional actions.
     */
    @Override
    public abstract void onActionClicked(@NonNull Action action);

    /**
     * Handles key events and returns true if handled.  A subclass may override this to provide
     * additional support.
     */
    @Override
    public abstract boolean onKey(View v, int keyCode, KeyEvent event);

    private void updateControlsRow() {
        onMetadataChanged();
    }

    @Override
    public final boolean isPlaying() {
        return mPlayerAdapter.isPlaying();
    }

    @Override
    public void play() {
        mPlayerAdapter.play();
    }

    @Override
    public void pause() {
        mPlayerAdapter.pause();
    }

    @Override
    public void next() {
        mPlayerAdapter.next();
    }

    @Override
    public void previous() {
        mPlayerAdapter.previous();
    }

    protected static void notifyItemChanged(
            @NonNull ArrayObjectAdapter adapter,
            @NonNull Object object
    ) {
        int index = adapter.indexOf(object);
        if (index >= 0) {
            adapter.notifyArrayItemRangeChanged(index, 1);
        }
    }

    /**
     * May be overridden to add primary actions to the adapter. Default implementation add
     * {@link PlaybackControlsRow.PlayPauseAction}.
     *
     * @param primaryActionsAdapter The adapter to add primary {@link Action}s.
     */
    protected void onCreatePrimaryActions(@NonNull ArrayObjectAdapter primaryActionsAdapter) {
    }

    /**
     * May be overridden to add secondary actions to the adapter.
     *
     * @param secondaryActionsAdapter The adapter you need to add the {@link Action}s to.
     */
    protected void onCreateSecondaryActions(@NonNull ArrayObjectAdapter secondaryActionsAdapter) {
    }

    @CallSuper
    protected void onUpdateProgress() {
        if (mControlsRow != null) {
            mControlsRow.setCurrentPosition(mPlayerAdapter.isPrepared()
                    ? getCurrentPosition() : -1);
        }
    }

    @CallSuper
    protected void onUpdateBufferedProgress() {
        if (mControlsRow != null) {
            mControlsRow.setBufferedPosition(mPlayerAdapter.getBufferedPosition());
        }
    }

    @CallSuper
    protected void onUpdateDuration() {
        if (mControlsRow != null) {
            mControlsRow.setDuration(
                    mPlayerAdapter.isPrepared() ? mPlayerAdapter.getDuration() : -1);
        }
    }

    /**
     * @return The duration of the media item in milliseconds.
     */
    public final long getDuration() {
        return mPlayerAdapter.getDuration();
    }

    /**
     * @return The current position of the media item in milliseconds.
     */
    public long getCurrentPosition() {
        return mPlayerAdapter.getCurrentPosition();
    }

    /**
     * @return The current buffered position of the media item in milliseconds.
     */
    public final long getBufferedPosition() {
        return mPlayerAdapter.getBufferedPosition();
    }

    @Override
    public final boolean isPrepared() {
        return mPlayerAdapter.isPrepared();
    }

    /**
     * Event when ready state for play changes.
     */
    @CallSuper
    protected void onPreparedStateChanged() {
        onUpdateDuration();
        List<PlayerCallback> callbacks = getPlayerCallbacks();
        if (callbacks != null) {
            for (int i = 0, size = callbacks.size(); i < size; i++) {
                callbacks.get(i).onPreparedStateChanged(this);
            }
        }
    }

    /**
     * Sets the drawable representing cover image. The drawable will be rendered by default
     * description presenter in
     * {@link PlaybackTransportRowPresenter#setDescriptionPresenter(Presenter)}.
     * @param cover The drawable representing cover image.
     */
    public void setArt(@Nullable Drawable cover) {
        if (mCover == cover) {
            return;
        }
        this.mCover = cover;
        mControlsRow.setImageDrawable(mCover);
        if (getHost() != null) {
            getHost().notifyPlaybackRowChanged();
        }
    }

    /**
     * @return The drawable representing cover image.
     */
    @Nullable
    public Drawable getArt() {
        return mCover;
    }

    /**
     * Sets the media subtitle. The subtitle will be rendered by default description presenter
     * {@link PlaybackTransportRowPresenter#setDescriptionPresenter(Presenter)}.
     * @param subtitle Subtitle to set.
     */
    public void setSubtitle(@Nullable CharSequence subtitle) {
        if (TextUtils.equals(subtitle, mSubtitle)) {
            return;
        }
        mSubtitle = subtitle;
        if (getHost() != null) {
            getHost().notifyPlaybackRowChanged();
        }
    }

    /**
     * Return The media subtitle.
     */
    @Nullable
    public CharSequence getSubtitle() {
        return mSubtitle;
    }

    /**
     * Sets the media title. The title will be rendered by default description presenter
     * {@link PlaybackTransportRowPresenter#setDescriptionPresenter(Presenter)}.
     */
    public void setTitle(@Nullable CharSequence title) {
        if (TextUtils.equals(title, mTitle)) {
            return;
        }
        mTitle = title;
        if (getHost() != null) {
            getHost().notifyPlaybackRowChanged();
        }
    }

    /**
     * Returns the title of the media item.
     */
    @Nullable
    public CharSequence getTitle() {
        return mTitle;
    }

    /**
     * Event when metadata changed
     */
    protected void onMetadataChanged() {
        if (mControlsRow == null) {
            return;
        }

        if (DEBUG) Log.v(TAG, "updateRowMetadata");

        mControlsRow.setImageDrawable(getArt());
        mControlsRow.setDuration(getDuration());
        mControlsRow.setCurrentPosition(getCurrentPosition());

        if (getHost() != null) {
            getHost().notifyPlaybackRowChanged();
        }
    }

    /**
     * Event when play state changed.
     */
    @CallSuper
    protected void onPlayStateChanged() {
        List<PlayerCallback> callbacks = getPlayerCallbacks();
        if (callbacks != null) {
            for (int i = 0, size = callbacks.size(); i < size; i++) {
                callbacks.get(i).onPlayStateChanged(this);
            }
        }
    }

    /**
     * Event when play finishes, subclass may handling repeat mode here.
     */
    @CallSuper
    protected void onPlayCompleted() {
        List<PlayerCallback> callbacks = getPlayerCallbacks();
        if (callbacks != null) {
            for (int i = 0, size = callbacks.size(); i < size; i++) {
                callbacks.get(i).onPlayCompleted(this);
            }
        }
    }

    /**
     * Seek media to a new position.
     * @param position New position.
     */
    public final void seekTo(long position) {
        mPlayerAdapter.seekTo(position);
    }

    /**
     * Returns a bitmask of actions supported by the media player.
     */
    public long getSupportedActions() {
        return mPlayerAdapter.getSupportedActions();
    }
}