package com.example.android.tvleanback.ui; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.PlaybackState; import android.os.Build.VERSION_CODES; import android.os.Handler; import android.support.annotation.NonNull; import android.support.v17.leanback.app.PlaybackControlGlue; 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.PlaybackControlsRowPresenter; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import android.util.Log; import android.view.View; import com.example.android.tvleanback.BuildConfig; import com.example.android.tvleanback.model.Video; class PlaybackControlHelper extends PlaybackControlGlue { private static final String TAG = "PlaybackControlHelper"; private static final boolean DEBUG = BuildConfig.DEBUG; private static final int[] SEEK_SPEEDS = {2}; // A single seek speed for fast-forward / rewind. private static final int DEFAULT_UPDATE_PERIOD = 500; private static final int UPDATE_PERIOD = 16; Drawable mMediaArt; private PlaybackOverlayFragment mFragment; private final MediaController mMediaController; private final MediaController.TransportControls mTransportControls; private PlaybackControlsRow.RepeatAction mRepeatAction; private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction; private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction; private PlaybackControlsRow.FastForwardAction mFastForwardAction; private PlaybackControlsRow.RewindAction mRewindAction; private PlaybackControlsRow.PictureInPictureAction mPipAction; private Video mVideo; private Handler mHandler = new Handler(); private Runnable mUpdateProgressRunnable; public PlaybackControlHelper(Context context, PlaybackOverlayFragment fragment, Video video) { super(context, fragment, SEEK_SPEEDS); mFragment = fragment; mVideo = video; mMediaController = mFragment.getActivity().getMediaController(); mTransportControls = mMediaController.getTransportControls(); mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context); mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.OUTLINE); mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context); mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.OUTLINE); mRepeatAction = new PlaybackControlsRow.RepeatAction(context); mPipAction = new PlaybackControlsRow.PictureInPictureAction(context); } public MediaController.Callback createMediaControllerCallback() { return new MediaControllerCallback(); } @Override public PlaybackControlsRowPresenter createControlsRowAndPresenter() { PlaybackControlsRowPresenter presenter = super.createControlsRowAndPresenter(); ArrayObjectAdapter adapter = new ArrayObjectAdapter(new ControlButtonPresenterSelector()); getControlsRow().setSecondaryActionsAdapter(adapter); mFastForwardAction = (PlaybackControlsRow.FastForwardAction) getPrimaryActionsAdapter() .lookup(ACTION_FAST_FORWARD); mRewindAction = (PlaybackControlsRow.RewindAction) getPrimaryActionsAdapter() .lookup(ACTION_REWIND); adapter.add(mThumbsDownAction); adapter.add(mRepeatAction); adapter.add(mThumbsUpAction); if (PlaybackOverlayActivity.supportsPictureInPicture(getContext())) { adapter.add(mPipAction); } presenter.setOnActionClickedListener(new OnActionClickedListener() { @Override public void onActionClicked(Action action) { dispatchAction(action); } }); return presenter; } @Override public void enableProgressUpdating(boolean enable) { mHandler.removeCallbacks(mUpdateProgressRunnable); if (enable) { mHandler.post(mUpdateProgressRunnable); } } @Override public int getUpdatePeriod() { View view = mFragment.getView(); int totalTime = getControlsRow().getTotalTime(); if (view == null || totalTime <= 0 || view.getWidth() == 0) { return DEFAULT_UPDATE_PERIOD; } return Math.max(UPDATE_PERIOD, totalTime / view.getWidth()); } @Override public void updateProgress() { if (mUpdateProgressRunnable == null) { mUpdateProgressRunnable = new Runnable() { @Override public void run() { int totalTime = getControlsRow().getTotalTime(); int currentTime = getCurrentPosition(); getControlsRow().setCurrentTime(currentTime); int progress = (int) mFragment.getBufferedPosition(); getControlsRow().setBufferedProgress(progress); if (totalTime > 0 && totalTime <= currentTime) { stopProgressAnimation(); } else { updateProgress(); } } }; } mHandler.postDelayed(mUpdateProgressRunnable, getUpdatePeriod()); } @Override public boolean hasValidMedia() { return mVideo != null; } @Override public boolean isMediaPlaying() { if (mMediaController.getPlaybackState() == null) { return false; } int state = mMediaController.getPlaybackState().getState(); return (state == PlaybackState.STATE_BUFFERING || state == PlaybackState.STATE_CONNECTING || state == PlaybackState.STATE_PLAYING || state == PlaybackState.STATE_FAST_FORWARDING || state == PlaybackState.STATE_REWINDING || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS || state == PlaybackState.STATE_SKIPPING_TO_NEXT); } @Override public CharSequence getMediaTitle() { return mVideo.title; } @Override public CharSequence getMediaSubtitle() { return mVideo.description; } @Override public int getMediaDuration() { return (int) mFragment.getDuration(); } @Override public Drawable getMediaArt() { return mMediaArt; } @Override public long getSupportedActions() { return ACTION_PLAY_PAUSE | ACTION_FAST_FORWARD | ACTION_REWIND | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_NEXT; } @Override public int getCurrentSpeedId() { return isMediaPlaying() ? PLAYBACK_SPEED_NORMAL : PLAYBACK_SPEED_PAUSED; } @Override public int getCurrentPosition() { return (int) mFragment.getCurrentPosition(); } @Override protected void startPlayback(int speed) { if (getCurrentSpeedId() == speed) { return; } mTransportControls.play(); } @Override protected void pausePlayback() { mTransportControls.pause(); } @Override protected void skipToNext() { mTransportControls.skipToNext(); } @Override protected void skipToPrevious() { mTransportControls.skipToPrevious(); } @Override protected void onRowChanged(PlaybackControlsRow row) { // Do nothing. } @Override protected void onMetadataChanged() { MediaMetadata metadata = mFragment.getActivity().getMediaController().getMetadata(); if (metadata != null) { mVideo = new Video.VideoBuilder().buildFromMediaDesc(metadata.getDescription()); int duration = (int) metadata.getLong(MediaMetadata.METADATA_KEY_DURATION); getControlsRow().setTotalTime(duration); mMediaArt = new BitmapDrawable(mFragment.getResources(), metadata.getBitmap(MediaMetadata.METADATA_KEY_ART)); } super.onMetadataChanged(); } @TargetApi(VERSION_CODES.N) public void dispatchAction(Action action) { if (action instanceof PlaybackControlsRow.MultiAction) { PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action; multiAction.nextIndex(); notifyActionChanged(multiAction); } if (action == mFastForwardAction) { mTransportControls.fastForward(); } else if (action == mRewindAction) { mTransportControls.rewind(); } else if (action.getId() == mPipAction.getId()) { ((Activity) getContext()).enterPictureInPictureMode(); } else { super.onActionClicked(action); } } private void notifyActionChanged(PlaybackControlsRow.MultiAction action) { int index; index = getPrimaryActionsAdapter().indexOf(action); if (index >= 0) { getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1); } else { index = getSecondaryActionsAdapter().indexOf(action); if (index >= 0) { getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1); } } } private void stopProgressAnimation() { if (mHandler != null && mUpdateProgressRunnable != null) { mHandler.removeCallbacks(mUpdateProgressRunnable); mUpdateProgressRunnable = null; } } private SparseArrayObjectAdapter getPrimaryActionsAdapter() { return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter(); } private ArrayObjectAdapter getSecondaryActionsAdapter() { return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter(); } private class MediaControllerCallback extends MediaController.Callback { @Override public void onPlaybackStateChanged(@NonNull PlaybackState state) { // Update your UI to reflect the new state. Do not change media playback here. if (DEBUG) Log.d(TAG, "Playback state changed: " + state.getState()); int nextState = state.getState(); if (nextState != PlaybackState.STATE_NONE) { updateProgress(); } onStateChanged(); } @Override public void onMetadataChanged(MediaMetadata metadata) { PlaybackControlHelper.this.onMetadataChanged(); // Update metadata on controls. mFragment.updatePlaybackRow(); } } }