package com.sregg.android.tv.spotifyPlayer.controllers;
import android.content.Context;
import android.media.AudioManager;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.crashlytics.android.answers.Answers;
import com.crashlytics.android.answers.CustomEvent;
import com.spotify.sdk.android.player.ConnectionStateCallback;
import com.spotify.sdk.android.player.PlayConfig;
import com.spotify.sdk.android.player.PlaybackBitrate;
import com.spotify.sdk.android.player.Player;
import com.spotify.sdk.android.player.PlayerNotificationCallback;
import com.spotify.sdk.android.player.PlayerState;
import com.spotify.sdk.android.player.PlayerStateCallback;
import com.spotify.sdk.android.player.Spotify;
import com.sregg.android.tv.spotifyPlayer.BusProvider;
import com.sregg.android.tv.spotifyPlayer.Constants;
import com.sregg.android.tv.spotifyPlayer.SpotifyTvApplication;
import com.sregg.android.tv.spotifyPlayer.enums.Control;
import com.sregg.android.tv.spotifyPlayer.events.ContentState;
import com.sregg.android.tv.spotifyPlayer.events.OnPause;
import com.sregg.android.tv.spotifyPlayer.events.OnPlay;
import com.sregg.android.tv.spotifyPlayer.events.OnShuffleChanged;
import com.sregg.android.tv.spotifyPlayer.events.OnTrackChanged;
import com.sregg.android.tv.spotifyPlayer.events.PlayerStateChanged;
import com.sregg.android.tv.spotifyPlayer.settings.UserPreferences;
import com.sregg.android.tv.spotifyPlayer.utils.Utils;
import java.util.ArrayList;
import java.util.List;
import kaaes.spotify.webapi.android.SpotifyService;
import kaaes.spotify.webapi.android.models.Track;
import kaaes.spotify.webapi.android.models.TrackSimple;
import retrofit.Callback;
import retrofit.RetrofitError;
import retrofit.client.Response;
import timber.log.Timber;
/**
* <p>Plays tracks, playlists and albums into the Spotify SDK</p>
* <p>Fires OTTO playback events like {@link com.sregg.android.tv.spotifyPlayer.events.OnPlay}, {@link com.sregg.android.tv.spotifyPlayer.events.OnPause}, etc..</p>
* <p>Also updates the Now Playing Card in the Home Screen</p>
*/
public class SpotifyPlayerController implements PlayerNotificationCallback, ConnectionStateCallback, AudioManager.OnAudioFocusChangeListener {
public static final String TAG = "SpotifyPlayerController";
private static final int SKIP_DURATION_MS = 10 * 1000;
private final Player mPlayer;
private final SpotifyService mSpotifyService;
private final Handler mHandler;
private final MediaPlayerSessionController mediaSessionController;
private final UserPreferences mUserPreferences;
private ContentState mContentState;
private final AudioManager audioManager;
private final Context context;
public SpotifyPlayerController(Player player, SpotifyService spotifyService) {
context = SpotifyTvApplication.getInstance().getApplicationContext();
mHandler = new Handler(context.getMainLooper());
mUserPreferences = UserPreferences.getInstance(context);
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mPlayer = player;
mPlayer.addPlayerNotificationCallback(this);
mPlayer.addConnectionStateCallback(this);
setPlayerBitrate(mUserPreferences.getBitrate());
setShuffle(mUserPreferences.getShuffle());
mediaSessionController = new MediaPlayerSessionController(context, this);
mSpotifyService = spotifyService;
// init playing state with dummy data
resetPlayingState();
}
public void resetPlayingState() {
mContentState = new ContentState("", "", null, null);
}
public void play(@Nullable String currentObjectUri, @Nullable List<String> trackUris, @Nullable List<TrackSimple> tracks) {
play(currentObjectUri, trackUris.get(0), trackUris, tracks);
}
public void play(@Nullable String currentObjectUri, @Nullable String currentTrackUri, @Nullable List<String> trackUris, @Nullable List<TrackSimple> tracks) {
if (trackUris == null) {
trackUris = new ArrayList<>();
}
if (!SpotifyTvApplication.isCurrentUserPremium()) {
Toast.makeText(SpotifyTvApplication.getInstance().getApplicationContext(), "You need a premium Spotify account to play music on this app", Toast.LENGTH_SHORT).show();
} else {
mContentState = new ContentState(currentObjectUri, currentTrackUri, trackUris, tracks);
PlayConfig playConfig = PlayConfig.createFor(trackUris);
int trackIndex = Math.max(0, trackUris.indexOf(currentTrackUri));
playConfig.withTrackIndex(trackIndex);
if (requestAudioFocus()) {
mPlayer.play(playConfig);
setShuffle(isShuffleOn());
}
}
}
private boolean requestAudioFocus() {
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
private void loseAudioFocus() {
audioManager.abandonAudioFocus(this);
}
public void togglePauseResume() {
mPlayer.getPlayerState(new PlayerStateCallback() {
@Override
public void onPlayerState(PlayerState playerState) {
if (playerState.playing) {
mPlayer.pause();
loseAudioFocus();
} else if (requestAudioFocus()) {
mPlayer.resume();
}
}
});
}
public
@Nullable
ContentState getPlayingState() {
return mContentState;
}
public void getPlayerState(PlayerStateCallback callback) {
mPlayer.getPlayerState(callback);
}
public void triggerPlayerStateUpdate() {
getPlayerState(new PlayerStateCallback() {
@Override
public void onPlayerState(PlayerState playerState) {
postPlayerStateChanged(playerState);
}
});
}
public boolean isShuffleOn() {
return mUserPreferences.getShuffle();
}
public void terminate() {
mediaSessionController.stopNowPlayingSession();
Spotify.destroyPlayer(mPlayer);
}
@Override
public void onPlaybackEvent(EventType eventType, PlayerState playerState) {
Log.d(TAG, String.format("%s - isPlaying: %s - trackUri: %s - positionInMs:%s",
eventType.name(), playerState.playing, playerState.trackUri, playerState.positionInMs));
mContentState.setCurrentTrackUri(playerState.trackUri);
switch (eventType) {
case PLAY:
BusProvider.post(new OnPlay(mContentState));
Answers.getInstance().logCustom(new CustomEvent(Constants.ANSWERS_EVENT_PLAYER_PLAY));
mediaSessionController.updateNowPlayingSession(playerState, mContentState);
postPlayerStateChanged(playerState);
break;
case PAUSE:
BusProvider.post(new OnPause(mContentState));
Answers.getInstance().logCustom(new CustomEvent(Constants.ANSWERS_EVENT_PLAYER_PAUSE));
loseAudioFocus();
mediaSessionController.updateNowPlayingSession(playerState, mContentState);
postPlayerStateChanged(playerState);
break;
case TRACK_CHANGED:
trackNowPlayingTrack(playerState.trackUri);
Answers.getInstance().logCustom(new CustomEvent(Constants.ANSWERS_EVENT_PLAYER_TRACK_CHANGE));
postPlayerStateChanged(playerState);
break;
case END_OF_CONTEXT:
mediaSessionController.stopNowPlayingSession();
resetPlayingState();
loseAudioFocus();
postPlayerStateChanged(playerState);
break;
}
}
private void postPlayerStateChanged(PlayerState playerState) {
BusProvider.post(new PlayerStateChanged(playerState, mContentState));
}
private void trackNowPlayingTrack(String currentTrackUri) {
// load track from id (from uri)
mSpotifyService.getTrack(Utils.getIdFromUri(currentTrackUri), new Callback<Track>() {
@Override
public void success(final Track track, Response response) {
mContentState.setCurrentTrack(track);
new Thread(new Runnable() {
@Override
public void run() {
mediaSessionController.updateNowPlayingMetadata(track);
trackLastFm(track);
mHandler.post(new Runnable() {
@Override
public void run() {
BusProvider.post(new OnTrackChanged(mContentState));
}
});
}
}).start();
}
@Override
public void failure(RetrofitError error) {
}
});
}
private void trackLastFm(final Track track) {
try {
LastFmApi.getInstance().scrobbleSpotifyTrack(track);
} catch (Exception e) {
// know issue in de.umass.lastfm.Track.Track.parseIntoScrobbleResult()
Log.w(TAG, "Error while scrobbling to last.fm", e);
}
}
@Override
public void onPlaybackError(ErrorType errorType, String s) {
Timber.w(s);
}
@Override
public void onLoggedIn() {
Timber.w("Logged In");
}
@Override
public void onLoggedOut() {
Timber.w("Logged Out");
}
@Override
public void onLoginFailed(Throwable throwable) {
Timber.e(throwable, "onLoginFailed");
}
@Override
public void onTemporaryError() {
}
@Override
public void onConnectionMessage(String s) {
Timber.w(s);
}
public void onControlClick(Control control) {
switch (control) {
case PLAY:
if (requestAudioFocus()) {
mPlayer.resume();
}
break;
case PAUSE:
mPlayer.pause();
loseAudioFocus();
break;
case NEXT:
mPlayer.skipToNext();
break;
case PREVIOUS:
mPlayer.skipToPrevious();
break;
case STOP:
stopPlayer();
break;
case SHUFFLE:
boolean shuffle = !isShuffleOn();
mUserPreferences.setShuffle(shuffle);
setShuffle(shuffle);
BusProvider.post(new OnShuffleChanged(shuffle));
// reload current object if not null
if (!TextUtils.isEmpty(mContentState.getCurrentObjectUri())) {
play(mContentState.getCurrentObjectUri(), mContentState.getTrackUrisQueue(), mContentState.getTracksQueue());
}
break;
case FAST_FORWARD:
mPlayer.getPlayerState(new PlayerStateCallback() {
@Override
public void onPlayerState(PlayerState playerState) {
final int fastForwardPosition = playerState.positionInMs + SKIP_DURATION_MS;
if (fastForwardPosition < playerState.durationInMs) {
mPlayer.seekToPosition(fastForwardPosition);
} else {
mPlayer.skipToNext();
}
}
});
break;
case REWIND:
mPlayer.getPlayerState(new PlayerStateCallback() {
@Override
public void onPlayerState(PlayerState playerState) {
final int currentPosition = playerState.positionInMs;
if (currentPosition < SKIP_DURATION_MS) {
mPlayer.seekToPosition(0);
} else {
final int rewindPosition = currentPosition - SKIP_DURATION_MS;
mPlayer.seekToPosition(rewindPosition);
}
}
});
break;
default:
break;
}
}
private void stopPlayer() {
mediaSessionController.stopNowPlayingSession();
if (mPlayer != null) {
mPlayer.pause();
mPlayer.clearQueue();
resetPlayingState();
}
loseAudioFocus();
BusProvider.post(new OnTrackChanged(mContentState));
}
public void setPlayerBitrate(PlaybackBitrate selectedBitrate) {
if (mPlayer != null) {
mPlayer.setPlaybackBitrate(selectedBitrate);
}
}
public void setShuffle(boolean shuffle) {
if (mPlayer != null) {
mPlayer.setShuffle(shuffle);
}
}
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
Timber.d("audio focus gained");
break;
case AudioManager.AUDIOFOCUS_LOSS:
Timber.d("audio focus lossed");
stopPlayer();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
Timber.d("audio focus lossed transient");
if (mPlayer != null) {
mPlayer.pause();
}
break;
}
}
}