public class

PlaybackTransportControlGlue<T extends PlayerAdapter>

extends PlaybackBaseControlGlue<PlayerAdapter>

 java.lang.Object

androidx.leanback.media.PlaybackGlue

androidx.leanback.media.PlaybackBaseControlGlue<PlayerAdapter>

↳androidx.leanback.media.PlaybackTransportControlGlue<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.PlaybackTransportControlGlue android.support.v17.leanback.media.PlaybackTransportControlGlue

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 in that manages interaction between the leanback UI components PlaybackControlsRow PlaybackTransportRowPresenter and a functional PlayerAdapter which represents the underlying media player.

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

The glue has two actions bar: primary actions bar and secondary actions bar. App can provide additional actions by overriding PlaybackTransportControlGlue.onCreatePrimaryActions(ArrayObjectAdapter) and / or PlaybackBaseControlGlue.onCreateSecondaryActions(ArrayObjectAdapter) and respond to actions by override PlaybackTransportControlGlue.onActionClicked(Action).

It's also subclass's responsibility to implement the "repeat mode" in PlaybackBaseControlGlue.onPlayCompleted().

Apps calls PlaybackTransportControlGlue.setSeekProvider(PlaybackSeekDataProvider) to provide seek data. If the PlaybackGlueHost is instance of PlaybackSeekUi, the provider will be passed to PlaybackGlueHost to render thumb bitmaps.

Sample Code:

 public class MyVideoFragment extends VideoFragment {
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         PlaybackTransportControlGlue playerGlue =
                 new PlaybackTransportControlGlue(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
from PlaybackBaseControlGlue<T>ACTION_CUSTOM_LEFT_FIRST, ACTION_CUSTOM_RIGHT_FIRST, ACTION_FAST_FORWARD, ACTION_PLAY_PAUSE, ACTION_REPEAT, ACTION_REWIND, ACTION_SHUFFLE, ACTION_SKIP_TO_NEXT, ACTION_SKIP_TO_PREVIOUS
Constructors
publicPlaybackTransportControlGlue(Context context, PlayerAdapter impl)

Constructor for the glue.

Methods
public final PlaybackSeekDataProvidergetSeekProvider()

Get seek data provider used during user seeking.

public final booleanisSeekEnabled()

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 PlaybackRowPresenteronCreateRowPresenter()

protected voidonDetachedFromHost()

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

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

Handles key events and returns true if handled.

protected voidonPlayStateChanged()

Event when play state changed.

protected voidonUpdateProgress()

public voidsetControlsRow(PlaybackControlsRow controlsRow)

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

public final voidsetSeekEnabled(boolean seekEnabled)

Enable or disable seek when PlaybackTransportControlGlue.getSeekProvider() is null.

public final voidsetSeekProvider(PlaybackSeekDataProvider seekProvider)

Set seek data provider used during user seeking.

from PlaybackBaseControlGlue<T>getArt, getBufferedPosition, getControlsRow, getCurrentPosition, getDuration, getPlaybackRowPresenter, getPlayerAdapter, getSubtitle, getSupportedActions, getTitle, isControlsOverlayAutoHideEnabled, isPlaying, isPrepared, next, notifyItemChanged, onCreateSecondaryActions, onHostStart, onHostStop, onMetadataChanged, onPlayCompleted, onPreparedStateChanged, onUpdateBufferedProgress, onUpdateDuration, pause, play, 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

Constructors

public PlaybackTransportControlGlue(Context context, PlayerAdapter impl)

Constructor for the glue.

Parameters:

context:
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()

protected void onAttachedToHost(PlaybackGlueHost host)

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

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 void onUpdateProgress()

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.

protected void onPlayStateChanged()

Event when play state changed.

public final void setSeekProvider(PlaybackSeekDataProvider seekProvider)

Set seek data provider used during user seeking.

Parameters:

seekProvider: Seek data provider used during user seeking.

public final PlaybackSeekDataProvider getSeekProvider()

Get seek data provider used during user seeking.

Returns:

Seek data provider used during user seeking.

public final void setSeekEnabled(boolean seekEnabled)

Enable or disable seek when PlaybackTransportControlGlue.getSeekProvider() is null. When true, PlayerAdapter.seekTo(long) will be called during user seeking.

Parameters:

seekEnabled: True to enable seek, false otherwise

public final boolean isSeekEnabled()

Returns:

True if seek is enabled without PlaybackSeekDataProvider, false otherwise.

Source

/*
 * Copyright (C) 2016 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.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;

import androidx.annotation.NonNull;
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.PlaybackRowPresenter;
import androidx.leanback.widget.PlaybackSeekDataProvider;
import androidx.leanback.widget.PlaybackSeekUi;
import androidx.leanback.widget.PlaybackTransportRowPresenter;
import androidx.leanback.widget.RowPresenter;

import java.lang.ref.WeakReference;

/**
 * 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 in that manages interaction between the
 * leanback UI components {@link PlaybackControlsRow} {@link PlaybackTransportRowPresenter}
 * and a functional {@link PlayerAdapter} which represents the underlying
 * media player.
 *
 * <p>App must pass a {@link PlayerAdapter} in constructor for a specific
 * implementation e.g. a {@link MediaPlayerAdapter}.
 * </p>
 *
 * <p>The glue has two actions bar: primary actions bar and secondary actions bar. App
 * can provide additional actions by overriding {@link #onCreatePrimaryActions} and / or
 * {@link #onCreateSecondaryActions} and respond to actions by override
 * {@link #onActionClicked(Action)}.
 * </p>
 *
 * <p> It's also subclass's responsibility to implement the "repeat mode" in
 * {@link #onPlayCompleted()}.
 * </p>
 *
 * <p>
 * Apps calls {@link #setSeekProvider(PlaybackSeekDataProvider)} to provide seek data. If the
 * {@link PlaybackGlueHost} is instance of {@link PlaybackSeekUi}, the provider will be passed to
 * PlaybackGlueHost to render thumb bitmaps.
 * </p>
 * Sample Code:
 * <pre><code>
 * public class MyVideoFragment extends VideoFragment {
 *     &#64;Override
 *     public void onCreate(Bundle savedInstanceState) {
 *         super.onCreate(savedInstanceState);
 *         PlaybackTransportControlGlue<MediaPlayerAdapter> playerGlue =
 *                 new PlaybackTransportControlGlue(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 PlaybackTransportControlGlue<T extends PlayerAdapter>
        extends PlaybackBaseControlGlue<T> {

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

    static final int MSG_UPDATE_PLAYBACK_STATE = 100;
    static final int UPDATE_PLAYBACK_STATE_DELAY_MS = 2000;

    PlaybackSeekDataProvider mSeekProvider;
    boolean mSeekEnabled;

    static class UpdatePlaybackStateHandler extends Handler {
        @Override
        @SuppressWarnings("unchecked")
        public void handleMessage(Message msg) {
            if (msg.what == MSG_UPDATE_PLAYBACK_STATE) {
                PlaybackTransportControlGlue glue =
                        ((WeakReference<PlaybackTransportControlGlue>) msg.obj).get();
                if (glue != null) {
                    glue.onUpdatePlaybackState();
                }
            }
        }
    }

    static final Handler sHandler = new UpdatePlaybackStateHandler();

    final WeakReference<PlaybackBaseControlGlue> mGlueWeakReference =
            new WeakReference<>(this);

    /**
     * Constructor for the glue.
     *
     * @param context
     * @param impl Implementation to underlying media player.
     */
    public PlaybackTransportControlGlue(Context context, T impl) {
        super(context, impl);
    }

    @Override
    public void setControlsRow(@NonNull PlaybackControlsRow controlsRow) {
        super.setControlsRow(controlsRow);
        sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
        onUpdatePlaybackState();
    }

    @Override
    protected void onCreatePrimaryActions(@NonNull ArrayObjectAdapter primaryActionsAdapter) {
        primaryActionsAdapter.add(mPlayPauseAction =
                new PlaybackControlsRow.PlayPauseAction(getContext()));
    }

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

        PlaybackTransportRowPresenter rowPresenter = new PlaybackTransportRowPresenter() {
            @Override
            protected void onBindRowViewHolder(
                    @NonNull RowPresenter.ViewHolder vh,
                    @NonNull Object item
            ) {
                super.onBindRowViewHolder(vh, item);
                vh.setOnKeyListener(PlaybackTransportControlGlue.this);
            }
            @Override
            protected void onUnbindRowViewHolder(@NonNull RowPresenter.ViewHolder vh) {
                super.onUnbindRowViewHolder(vh);
                vh.setOnKeyListener(null);
            }
        };
        rowPresenter.setDescriptionPresenter(detailsPresenter);
        return rowPresenter;
    }

    @Override
    protected void onAttachedToHost(@NonNull PlaybackGlueHost host) {
        super.onAttachedToHost(host);

        if (host instanceof PlaybackSeekUi) {
            ((PlaybackSeekUi) host).setPlaybackSeekUiClient(mPlaybackSeekUiClient);
        }
    }

    @Override
    protected void onDetachedFromHost() {
        super.onDetachedFromHost();

        if (getHost() instanceof PlaybackSeekUi) {
            ((PlaybackSeekUi) getHost()).setPlaybackSeekUiClient(null);
        }
    }

    @Override
    protected void onUpdateProgress() {
        if (!mPlaybackSeekUiClient.mIsSeek) {
            super.onUpdateProgress();
        }
    }

    @Override
    public void onActionClicked(@NonNull Action action) {
        dispatchAction(action, null);
    }

    @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:
                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);

        // Sync playback state after a delay
        sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
        sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_UPDATE_PLAYBACK_STATE,
                mGlueWeakReference), UPDATE_PLAYBACK_STATE_DELAY_MS);
    }

    /**
     * Called when the given action is invoked, either by click or keyevent.
     */
    boolean dispatchAction(Action action, KeyEvent keyEvent) {
        boolean handled = false;
        if (action instanceof PlaybackControlsRow.PlayPauseAction) {
            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 && mIsPlaying) {
                mIsPlaying = false;
                pause();
            } else if (canPlay && !mIsPlaying) {
                mIsPlaying = true;
                play();
            }
            onUpdatePlaybackStatusAfterUserAction();
            handled = true;
        } else if (action instanceof PlaybackControlsRow.SkipNextAction) {
            next();
            handled = true;
        } else if (action instanceof PlaybackControlsRow.SkipPreviousAction) {
            previous();
            handled = true;
        }
        return handled;
    }

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

        if (sHandler.hasMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference)) {
            sHandler.removeMessages(MSG_UPDATE_PLAYBACK_STATE, mGlueWeakReference);
            if (mPlayerAdapter.isPlaying() != mIsPlaying) {
                if (DEBUG) Log.v(TAG, "Status expectation mismatch, delaying update");
                sHandler.sendMessageDelayed(sHandler.obtainMessage(MSG_UPDATE_PLAYBACK_STATE,
                        mGlueWeakReference), UPDATE_PLAYBACK_STATE_DELAY_MS);
            } else {
                if (DEBUG) Log.v(TAG, "Update state matches expectation");
                onUpdatePlaybackState();
            }
        } else {
            onUpdatePlaybackState();
        }

        super.onPlayStateChanged();
    }

    void onUpdatePlaybackState() {
        mIsPlaying = mPlayerAdapter.isPlaying();
        updatePlaybackState(mIsPlaying);
    }

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

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

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

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

    final SeekUiClient mPlaybackSeekUiClient = new SeekUiClient();

    class SeekUiClient extends PlaybackSeekUi.Client {
        boolean mPausedBeforeSeek;
        long mPositionBeforeSeek;
        long mLastUserPosition;
        boolean mIsSeek;

        @Override
        public PlaybackSeekDataProvider getPlaybackSeekDataProvider() {
            return mSeekProvider;
        }

        @Override
        public boolean isSeekEnabled() {
            return mSeekProvider != null || mSeekEnabled;
        }

        @Override
        public void onSeekStarted() {
            mIsSeek = true;
            mPausedBeforeSeek = !isPlaying();
            mPlayerAdapter.setProgressUpdatingEnabled(true);
            // if we seek thumbnails, we don't need save original position because current
            // position is not changed during seeking.
            // otherwise we will call seekTo() and may need to restore the original position.
            mPositionBeforeSeek = mSeekProvider == null ? mPlayerAdapter.getCurrentPosition() : -1;
            mLastUserPosition = -1;
            pause();
        }

        @Override
        public void onSeekPositionChanged(long pos) {
            if (mSeekProvider == null) {
                mPlayerAdapter.seekTo(pos);
            } else {
                mLastUserPosition = pos;
            }
            if (mControlsRow != null) {
                mControlsRow.setCurrentPosition(pos);
            }
        }

        @Override
        public void onSeekFinished(boolean cancelled) {
            if (!cancelled) {
                if (mLastUserPosition >= 0) {
                    seekTo(mLastUserPosition);
                }
            } else {
                if (mPositionBeforeSeek >= 0) {
                    seekTo(mPositionBeforeSeek);
                }
            }
            mIsSeek = false;
            if (!mPausedBeforeSeek) {
                play();
            } else {
                mPlayerAdapter.setProgressUpdatingEnabled(false);
                // we neeed update UI since PlaybackControlRow still saves previous position.
                onUpdateProgress();
            }
        }
    };

    /**
     * Set seek data provider used during user seeking.
     * @param seekProvider Seek data provider used during user seeking.
     */
    public final void setSeekProvider(PlaybackSeekDataProvider seekProvider) {
        mSeekProvider = seekProvider;
    }

    /**
     * Get seek data provider used during user seeking.
     * @return Seek data provider used during user seeking.
     */
    public final PlaybackSeekDataProvider getSeekProvider() {
        return mSeekProvider;
    }

    /**
     * Enable or disable seek when {@link #getSeekProvider()} is null. When true,
     * {@link PlayerAdapter#seekTo(long)} will be called during user seeking.
     *
     * @param seekEnabled True to enable seek, false otherwise
     */
    public final void setSeekEnabled(boolean seekEnabled) {
        mSeekEnabled = seekEnabled;
    }

    /**
     * @return True if seek is enabled without {@link PlaybackSeekDataProvider}, false otherwise.
     */
    public final boolean isSeekEnabled() {
        return mSeekEnabled;
    }
}