package com.teocci.ytinbg.playback; import android.content.res.Resources; import android.media.session.PlaybackState; import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import com.teocci.ytinbg.interfaces.Playback; import com.teocci.ytinbg.interfaces.PlaybackServiceCallback; import com.teocci.ytinbg.model.YouTubeVideo; import com.teocci.ytinbg.utils.LogHelper; import java.util.List; import androidx.annotation.NonNull; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; import static android.support.v4.media.session.PlaybackStateCompat.ERROR_CODE_SKIP_LIMIT_REACHED; import static android.support.v4.media.session.PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN; import static android.support.v4.media.session.PlaybackStateCompat.STATE_ERROR; import static android.support.v4.media.session.PlaybackStateCompat.STATE_PAUSED; import static android.support.v4.media.session.PlaybackStateCompat.STATE_PLAYING; /** * Created by teocci. * Manage the interactions among the container service, the queue manager and the actual playback. * * @author [email protected] on 2017-Jun-08 */ public class PlaybackManager implements Playback.Callback { private static final String TAG = LogHelper.makeLogTag(PlaybackManager.class); private QueueManager queueManager; private Resources resources; private Playback playback; private PlaybackServiceCallback serviceCallback; private MediaSessionCallback mediaSessionCallback; public PlaybackManager(PlaybackServiceCallback serviceCallback, Resources resources, QueueManager queueManager, Playback playback) { this.serviceCallback = serviceCallback; this.resources = resources; this.queueManager = queueManager; this.mediaSessionCallback = new MediaSessionCallback(); this.playback = playback; this.playback.setCallback(this); } public Playback getPlayback() { return playback; } public MediaSessionCompat.Callback getMediaSessionCallback() { return mediaSessionCallback; } /** * Handle a request to play music */ public void handlePlayRequest() { LogHelper.e(TAG, "handlePlayRequest: getState=" + playback.getState()); YouTubeVideo currentYouTubeVideo = queueManager.getCurrentVideo(); if (currentYouTubeVideo != null) { serviceCallback.onPlaybackStart(); playback.play(currentYouTubeVideo); } } /** * Handle a request to pause music */ public void handlePauseRequest() { LogHelper.e(TAG, "handlePauseRequest: getState=" + playback.getState()); if (playback.isPlaying()) { playback.pause(); serviceCallback.onPlaybackStop(); } } /** * Handle a request to stop music * * @param withError Error message in case the stop has an unexpected cause. The error * message will be set in the PlaybackState and will be visible to * MediaController clients. */ public void handleStopRequest(String withError) { LogHelper.d(TAG, "handleStopRequest: mState=" + playback.getState() + " error=", withError); playback.stop(true); serviceCallback.onPlaybackStop(); updatePlaybackState(withError); } /** * Update the current media player state, one of the following, optionally showing an error message. * <ul> * <li> {@link PlaybackState#STATE_NONE}</li> * <li> {@link PlaybackState#STATE_STOPPED}</li> * <li> {@link PlaybackState#STATE_PLAYING}</li> * <li> {@link PlaybackState#STATE_PAUSED}</li> * <li> {@link PlaybackState#STATE_FAST_FORWARDING}</li> * <li> {@link PlaybackState#STATE_REWINDING}</li> * <li> {@link PlaybackState#STATE_BUFFERING}</li> * <li> {@link PlaybackState#STATE_ERROR}</li> * <li> {@link PlaybackState#STATE_CONNECTING}</li> * <li> {@link PlaybackState#STATE_SKIPPING_TO_PREVIOUS}</li> * <li> {@link PlaybackState#STATE_SKIPPING_TO_NEXT}</li> * <li> {@link PlaybackState#STATE_SKIPPING_TO_QUEUE_ITEM}</li> * </ul> * * @param error if not null, error message to present to the user. */ public void updatePlaybackState(String error) { LogHelper.d(TAG, "updatePlaybackState, playback state=" + playback.getState()); long position = PLAYBACK_POSITION_UNKNOWN; if (playback != null && playback.isConnected()) { position = playback.getCurrentStreamPosition(); } // Noinspection ResourceType PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat .Builder() .setActions(getAvailableActions()); int state = playback.getState(); // If there is an error message, send it to the playback state: if (error != null) { // Error states are really only supposed to be used for errors that cause playback to // stop unexpectedly and persist until the user takes action to fix it. stateBuilder.setErrorMessage(ERROR_CODE_SKIP_LIMIT_REACHED, error); state = STATE_ERROR; } // Noinspection ResourceType stateBuilder.setState(state, position, 1.0f, SystemClock.elapsedRealtime()); // Set the activeQueueItemId if the current index is valid. YouTubeVideo currentYouTubeVideo = queueManager.getCurrentVideo(); if (currentYouTubeVideo != null) { stateBuilder.setActiveQueueItemId(queueManager.getCurrentVideoIndex(currentYouTubeVideo.getId())); } serviceCallback.onPlaybackStateUpdated(stateBuilder.build()); if (state == STATE_PLAYING || state == STATE_PAUSED) { serviceCallback.onNotificationRequired(); } } private long getAvailableActions() { long actions = ACTION_PLAY_PAUSE | ACTION_PLAY_FROM_MEDIA_ID | ACTION_PLAY_FROM_SEARCH | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_NEXT; if (playback.isPlaying()) { actions |= ACTION_PAUSE; } else { actions |= ACTION_PLAY; } return actions; } /** * Implementation of the Playback.Callback interface */ @Override public void onCompletion() { // The media player finished playing the current song, so we go ahead // and start the next. if (queueManager.skipQueuePosition(1)) { handlePlayRequest(); queueManager.updateYouTubeVideo(); } else { // If skipping was not possible, we stop and release the resources: handleStopRequest(null); } } @Override public void onPlaybackStatusChanged(int state) { updatePlaybackState(null); } @Override public void onError(String error) { updatePlaybackState(error); } @Override public void setCurrentMediaId(String mediaId) { LogHelper.e(TAG, "setCurrentYouTubeVideoId", mediaId); // queueManager.setQueueFromMusic(mediaId); } /** * Switch to a different Playback instance, maintaining all playback state, if possible. * * @param playback switch to this playback */ public void switchToPlayback(Playback playback, boolean resumePlaying) { if (playback == null) { throw new IllegalArgumentException("Playback cannot be null"); } // Suspends current state. int oldState = this.playback.getState(); long pos = this.playback.getCurrentStreamPosition(); String currentMediaId = this.playback.getCurrentYouTubeVideoId(); this.playback.stop(false); playback.setCallback(this); playback.setCurrentYouTubeVideoId(currentMediaId); playback.seekTo(pos < 0 ? 0 : pos); playback.start(); // Swaps instance. this.playback = playback; switch (oldState) { case PlaybackStateCompat.STATE_BUFFERING: case PlaybackStateCompat.STATE_CONNECTING: case STATE_PAUSED: this.playback.pause(); break; case STATE_PLAYING: YouTubeVideo currentYouTubeVideo = queueManager.getCurrentVideo(); if (resumePlaying && currentYouTubeVideo != null) { LogHelper.e(TAG, "switchToPlayback: call | playback.play"); this.playback.play(currentYouTubeVideo); } else if (!resumePlaying) { this.playback.pause(); } else { this.playback.stop(true); } break; case PlaybackStateCompat.STATE_NONE: break; default: LogHelper.d(TAG, "Default called. Old state is ", oldState); } } public void initPlaylist(YouTubeVideo currentYouTubeVideo, List<YouTubeVideo> ytVideoList) { if (currentYouTubeVideo != null) { queueManager.setCurrentQueue(currentYouTubeVideo, ytVideoList); queueManager.updateYouTubeVideo(); } } public long getDuration() { long duration = this.playback.getDuration() > -1 ? this.playback.getDuration() : -1; LogHelper.e(TAG, "getDuration: " + duration); return duration; } public void updateYouTubeVideo() { queueManager.updateYouTubeVideo(); } public void setRepeatOption(int repeatMode) { playback.setRepeatMode(repeatMode); } private class MediaSessionCallback extends MediaSessionCompat.Callback { @Override public void onPlay() { LogHelper.d(TAG, "play"); if (queueManager.getCurrentVideo() != null) { handlePlayRequest(); } } @Override public void onSeekTo(long position) { LogHelper.d(TAG, "onSeekTo:", position); playback.seekTo((int) position); } @Override public void onPause() { LogHelper.d(TAG, "pause. current state=" + playback.getState()); handlePauseRequest(); } @Override public void onStop() { LogHelper.d(TAG, "stop. current state=" + playback.getState()); handleStopRequest(null); } @Override public void onSkipToNext() { LogHelper.d(TAG, "skipToNext"); if (queueManager.skipQueuePosition(1)) { handlePlayRequest(); } else { handleStopRequest("Cannot skip"); } queueManager.updateYouTubeVideo(); } @Override public void onSkipToPrevious() { if (queueManager.skipQueuePosition(-1)) { handlePlayRequest(); } else { handleStopRequest("Cannot skip"); } queueManager.updateYouTubeVideo(); } @Override public void onCustomAction(@NonNull String action, Bundle extras) { LogHelper.e(TAG, "Unsupported action: ", action); } /** * Handle free and contextual searches. * <p/> * All voice searches on Android Auto are sent to this method through a connected * {@link android.support.v4.media.session.MediaControllerCompat}. * <p/> * Threads and async handling: * Search, as a potentially slow operation, should run in another thread. * <p/> * Since this method runs on the main thread, most apps with non-trivial metadata * should defer the actual search to another thread (for example, by using * an {@link AsyncTask} as we do here). **/ @Override public void onPlayFromSearch(final String query, final Bundle extras) { LogHelper.d(TAG, "playFromSearch query=", query, " extras=", extras); // playback.setState(PlaybackStateCompat.STATE_CONNECTING); // boolean successSearch = queueManager.setQueueFromSearch(query, extras); // if (successSearch) { // handlePlayRequest(); // queueManager.updateYouTubeVideo(); // } else { // updatePlaybackState("Could not find music"); // } } } }