/* * 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 android.support.v17.leanback.media; import android.content.Context; import android.graphics.drawable.Drawable; import android.support.annotation.CallSuper; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.ControlButtonPresenterSelector; import android.support.v17.leanback.widget.OnActionClickedListener; import android.support.v17.leanback.widget.PlaybackControlsRow; import android.support.v17.leanback.widget.PlaybackRowPresenter; import android.support.v17.leanback.widget.PlaybackTransportRowPresenter; import android.support.v17.leanback.widget.Presenter; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; import android.view.View; 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. * *

The app must pass a {@link PlayerAdapter} in constructor for a specific * implementation e.g. a {@link MediaPlayerAdapter}. *

* *

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)}. *

* *

The subclass is responsible for implementing the "repeat mode" in * {@link #onPlayCompleted()}. *

* * @param Type of {@link PlayerAdapter} passed in constructor. */ public abstract class PlaybackBaseControlGlue extends PlaybackGlue implements OnActionClickedListener, View.OnKeyListener { 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); } } }; /** * 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(); } 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) { } void onUpdateProgress() { if (mControlsRow != null) { mControlsRow.setCurrentPosition(mPlayerAdapter.isPrepared() ? getCurrentPosition() : -1); } } void onUpdateBufferedProgress() { if (mControlsRow != null) { mControlsRow.setBufferedPosition(mPlayerAdapter.getBufferedPosition()); } } 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 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 */ void onMetadataChanged() { if (mControlsRow == null) { return; } if (DEBUG) Log.v(TAG, "updateRowMetadata"); mControlsRow.setImageDrawable(getArt()); mControlsRow.setDuration(mPlayerAdapter.getDuration()); mControlsRow.setCurrentPosition(getCurrentPosition()); if (getHost() != null) { getHost().notifyPlaybackRowChanged(); } } /** * Event when play state changed. */ @CallSuper protected void onPlayStateChanged() { List 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 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); } }