blob: a00be504b53e336bd06e850736dc56aebedbfbf8 [file] [log] [blame]
/*
* 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.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(PlayerAdapter wrapper) {
if (DEBUG) Log.v(TAG, "onPlayStateChanged");
PlaybackBaseControlGlue.this.onPlayStateChanged();
}
@Override
public void onCurrentPositionChanged(PlayerAdapter wrapper) {
if (DEBUG) Log.v(TAG, "onCurrentPositionChanged");
PlaybackBaseControlGlue.this.onUpdateProgress();
}
@Override
public void onBufferedPositionChanged(PlayerAdapter wrapper) {
if (DEBUG) Log.v(TAG, "onBufferedPositionChanged");
PlaybackBaseControlGlue.this.onUpdateBufferedProgress();
}
@Override
public void onDurationChanged(PlayerAdapter wrapper) {
if (DEBUG) Log.v(TAG, "onDurationChanged");
PlaybackBaseControlGlue.this.onUpdateDuration();
}
@Override
public void onPlayCompleted(PlayerAdapter wrapper) {
if (DEBUG) Log.v(TAG, "onPlayCompleted");
PlaybackBaseControlGlue.this.onPlayCompleted();
}
@Override
public void onPreparedStateChanged(PlayerAdapter wrapper) {
if (DEBUG) Log.v(TAG, "onPreparedStateChanged");
PlaybackBaseControlGlue.this.onPreparedStateChanged();
}
@Override
public void onVideoSizeChanged(PlayerAdapter wrapper, int width, int height) {
mVideoWidth = width;
mVideoHeight = height;
if (mPlayerCallback != null) {
mPlayerCallback.onVideoSizeChanged(width, height);
}
}
@Override
public void onError(PlayerAdapter wrapper, int errorCode, String errorMessage) {
mErrorSet = true;
mErrorCode = errorCode;
mErrorMessage = errorMessage;
if (mPlayerCallback != null) {
mPlayerCallback.onError(errorCode, errorMessage);
}
}
@Override
public void onBufferingStateChanged(PlayerAdapter wrapper, boolean start) {
mBuffering = start;
if (mPlayerCallback != null) {
mPlayerCallback.onBufferingStateChanged(start);
}
}
@Override
public void onMetadataChanged(PlayerAdapter wrapper) {
PlaybackBaseControlGlue.this.onMetadataChanged();
}
};
/**
* Constructor for the glue.
*
* @param context
* @param impl Implementation to underlying media player.
*/
public PlaybackBaseControlGlue(Context context, T impl) {
super(context);
mPlayerAdapter = impl;
mPlayerAdapter.setCallback(mAdapterCallback);
}
public final T getPlayerAdapter() {
return mPlayerAdapter;
}
@Override
protected void onAttachedToHost(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());
}
}
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(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(PlaybackRowPresenter presenter) {
mControlsRowPresenter = presenter;
}
/**
* Returns the playback controls row managed by the glue layer.
*/
public PlaybackControlsRow getControlsRow() {
return mControlsRow;
}
/**
* Returns the playback controls row Presenter managed by the glue layer.
*/
public PlaybackRowPresenter getPlaybackRowPresenter() {
return mControlsRowPresenter;
}
/**
* Handles action clicks. A subclass may override this add support for additional actions.
*/
@Override
public abstract void onActionClicked(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(ArrayObjectAdapter adapter, 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(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(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(Drawable cover) {
if (mCover == cover) {
return;
}
this.mCover = cover;
mControlsRow.setImageDrawable(mCover);
if (getHost() != null) {
getHost().notifyPlaybackRowChanged();
}
}
/**
* @return The drawable representing cover image.
*/
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(CharSequence subtitle) {
if (TextUtils.equals(subtitle, mSubtitle)) {
return;
}
mSubtitle = subtitle;
if (getHost() != null) {
getHost().notifyPlaybackRowChanged();
}
}
/**
* Return The media subtitle.
*/
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(CharSequence title) {
if (TextUtils.equals(title, mTitle)) {
return;
}
mTitle = title;
if (getHost() != null) {
getHost().notifyPlaybackRowChanged();
}
}
/**
* Returns the title of the media item.
*/
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();
}
}