PlaybackBaseControlGlue.java

/*
 * 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();
    }
}