package com.atomjack.vcfp.services;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.view.KeyEvent;
import com.atomjack.shared.NewLogger;
import com.atomjack.shared.PlayerState;
import com.atomjack.vcfp.FetchMediaImageTask;
import com.atomjack.vcfp.PlexHeaders;
import com.atomjack.vcfp.VoiceControlForPlexApplication;
import com.atomjack.vcfp.interfaces.ActiveConnectionHandler;
import com.atomjack.vcfp.interfaces.BitmapHandler;
import com.atomjack.vcfp.interfaces.MusicServiceListener;
import com.atomjack.vcfp.model.Connection;
import com.atomjack.vcfp.model.PlexClient;
import com.atomjack.vcfp.model.PlexMedia;
import com.atomjack.vcfp.model.PlexTrack;
import com.atomjack.vcfp.net.PlexHttpClient;
import com.atomjack.vcfp.receivers.RemoteControlReceiver;
import java.util.ArrayList;
public class LocalMusicService extends Service implements
MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener,
MediaPlayer.OnCompletionListener {
private NewLogger logger;
private MediaPlayer player;
private PlexTrack track;
private ArrayList<PlexTrack> playlist;
private int currentSongIdx;
private PlayerState currentState = PlayerState.STOPPED;
private Handler handler;
private final IBinder musicBind = new MusicBinder();
private MusicServiceListener musicServiceListener;
AudioManager audioManager;
ComponentName remoteControlReceiver;
private MediaSessionCompat mediaSession;
private long headsetDownTime = 0;
private long headsetUpTime = 0;
@Override
@SuppressWarnings("deprecation")
public void onCreate() {
super.onCreate();
logger = new NewLogger(this);
logger.d("onCreate");
player = new MediaPlayer();
currentSongIdx = 0;
handler = new Handler();
initMusicPlayer();
remoteControlReceiver = new ComponentName(getPackageName(), RemoteControlReceiver.class.getName());
mediaSession = new MediaSessionCompat(this, "VCFPRemoteControlReceiver", remoteControlReceiver, null);
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
PlaybackStateCompat state = new PlaybackStateCompat.Builder()
.setActions(PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_STOP)
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1, SystemClock.elapsedRealtime())
.build();
mediaSession.setPlaybackState(state);
Intent intent = new Intent(this, RemoteControlReceiver.class);
PendingIntent pintent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
mediaSession.setMediaButtonReceiver(pintent);
mediaSession.setActive(false);
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.requestAudioFocus(new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
logger.d("audio focus changed");
}
}, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
audioManager.registerMediaButtonEventReceiver(remoteControlReceiver);
}
private Runnable notPurchasedDisconnectTimer = new Runnable() {
@Override
public void run() {
logger.d("auto stopping");
doStop();
}
};
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// logger.d("onStartCommand: %s", intent.getAction());
if(intent.getAction() != null) {
if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_PLAY) || intent.getAction().equals(com.atomjack.shared.Intent.ACTION_PAUSE)) {
doPlayPause();
} else if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_STOP)) {
doStop();
} else if (intent.getAction().equals(com.atomjack.shared.Intent.ACTION_PREVIOUS)) {
doPrevious();
} else if(intent.getAction().equals(com.atomjack.shared.Intent.ACTION_NEXT)) {
doNext();
} else if(intent.getAction().equals(com.atomjack.shared.Intent.ACTION_DISCONNECT)) {
doStop();
} else if(intent.getAction().equals(com.atomjack.shared.Intent.ACTION_MEDIA_BUTTON)) {
handleMediaButton((KeyEvent)intent.getParcelableExtra(com.atomjack.shared.Intent.KEY_EVENT));
}
}
return Service.START_NOT_STICKY;
}
private void initMusicPlayer() {
player.setWakeMode(getApplicationContext(),
PowerManager.PARTIAL_WAKE_LOCK);
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.setOnPreparedListener(this);
player.setOnCompletionListener(this);
player.setOnErrorListener(this);
}
@SuppressWarnings("unchecked")
public void setPlaylist(ArrayList<? extends PlexMedia> playlist) {
this.playlist = (ArrayList<PlexTrack>)playlist;
}
public void setTrack(PlexTrack t) {
playlist = null;
track = t;
}
public class MusicBinder extends Binder {
public LocalMusicService getService() {
return LocalMusicService.this;
}
public void setListener(MusicServiceListener listener) {
musicServiceListener = listener;
}
}
public PlayerState getCurrentState() {
return currentState;
}
public void playSong() {
player.reset();
if(playlist != null)
track = playlist.get(currentSongIdx);
logger.d("Playing Track: %s", track.getTitle());
if(track != null) {
track.server.findServerConnection(new ActiveConnectionHandler() {
@Override
public void onSuccess(Connection connection) {
String url = String.format("%s%s", connection.uri, getTrackUrl(track));
try {
player.setDataSource(getApplicationContext(), Uri.parse(url));
player.prepareAsync();
musicServiceListener.onTrackChange(track);
VoiceControlForPlexApplication.getInstance().setNotification(PlexClient.getLocalPlaybackClient(), currentState, track, playlist, mediaSession);
new FetchMediaImageTask(track, 500, 500, track.getNotificationThumb(PlexMedia.IMAGE_KEY.WEAR_BACKGROUND), track.getImageKey(PlexMedia.IMAGE_KEY.WEAR_BACKGROUND), new BitmapHandler() {
@Override
public void onSuccess(Bitmap bitmap) {
mediaSession.setMetadata(new MediaMetadataCompat.Builder()
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, track.getArtist())
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, track.getAlbum())
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, track.getTitle())
.build()
);
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
handler.post(new Runnable() {
@Override
public void run() {
registerMediaSessionCallback();
mediaSession.setActive(true);
}
});
}
}).execute();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onFailure(int statusCode) {
}
});
}
}
public void reset() {
currentSongIdx = 0;
}
public void doPrevious() {
if(currentSongIdx > 0) {
player.stop();
currentSongIdx--;
playSong();
}
}
public void doNext() {
if(currentSongIdx+1 < playlist.size()) {
player.stop();
currentSongIdx++;
playSong();
}
}
public void doStop() {
player.stop();
handler.removeCallbacks(playerProgressUpdater);
PlexHttpClient.reportProgressToServer(track, player.getCurrentPosition(), PlayerState.STOPPED);
VoiceControlForPlexApplication.getInstance().cancelNotification();
musicServiceListener.onFinished();
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
mediaSession.setActive(false);
handler.removeCallbacks(notPurchasedDisconnectTimer);
stopSelf();
}
public void seek(int time) {
player.seekTo(time);
}
public void doPlay() {
logger.d("doPlay");
player.start();
currentState = PlayerState.PLAYING;
}
public void doPause() {
player.pause();
currentState = PlayerState.PAUSED;
}
public void doPlayPause() {
if(currentState == PlayerState.PLAYING) {
player.pause();
currentState = PlayerState.PAUSED;
} else {
player.start();
currentState = PlayerState.PLAYING;
}
VoiceControlForPlexApplication.getInstance().setNotification(PlexClient.getLocalPlaybackClient(), currentState, track, playlist, mediaSession);
}
public boolean isPlaying() {
return player.isPlaying();
}
public void setSong(int idx) {
currentSongIdx = idx;
}
private String getTrackUrl(PlexTrack track) {
// /library/parts/83301/file.mp3?X-Plex-Token=xxxxxxxxxxxxxxxxxxxx
String key = track.getPart().key;
if(track.server.accessToken != null)
key += String.format("?%s=%s", PlexHeaders.XPlexToken, track.server.accessToken);
return key;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
logger.d("onBind: %s", intent.getAction());
return musicBind;
}
@Override
public boolean onUnbind(Intent intent) {
player.stop();
player.release();
return false;
}
@Override
public void onCompletion(MediaPlayer mp) {
currentSongIdx++;
logger.d("onCompletion, current: %d, size: %d", currentSongIdx, playlist.size());
if(currentSongIdx == playlist.size()) {
// Last song
musicServiceListener.onFinished();
} else {
playSong();
}
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
return false;
}
@Override
public void onPrepared(MediaPlayer mp) {
logger.d("onPrepared");
currentState = PlayerState.BUFFERING;
player.start();
musicIsPlayingCheck.run();
}
private Runnable playerProgressUpdater = new Runnable() {
@Override
public void run() {
PlexHttpClient.reportProgressToServer(track, player.getCurrentPosition(), currentState);
musicServiceListener.onTimeUpdate(currentState, player.getCurrentPosition());
handler.postDelayed(this, 1000);
}
};
private Runnable musicIsPlayingCheck = new Runnable() {
@Override
public void run() {
if(player.getDuration() > 0) {
logger.d("Audio is playing");
currentState = PlayerState.PLAYING;
VoiceControlForPlexApplication.getInstance().setNotification(PlexClient.getLocalPlaybackClient(), currentState, track, playlist, mediaSession);
handler.postDelayed(playerProgressUpdater, 1000);
if(!VoiceControlForPlexApplication.getInstance().hasLocalmedia()) {
handler.removeCallbacks(notPurchasedDisconnectTimer);
handler.postDelayed(notPurchasedDisconnectTimer, 1000*60); // disconnect in 60 seconds
}
} else {
handler.postDelayed(musicIsPlayingCheck, 100);
}
}
};
public PlexTrack getTrack() {
return track;
}
public ArrayList<PlexTrack> getPlaylist() {
return playlist;
}
@Override
@SuppressWarnings("deprecation")
public void onDestroy() {
super.onDestroy();
logger.d("onDestroy");
mediaSession.release();
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
audioManager.unregisterMediaButtonEventReceiver(remoteControlReceiver);
}
}
// Do play/pause 501ms after headset button is clicked. If it is doubleclicked, this task will get canceled, and doNext() will be called instead
private Runnable handleSingleClick = new Runnable() {
@Override
public void run() {
logger.d("Single click");
doPlayPause();
}
};
public void handleMediaButton(KeyEvent event) {
switch (event.getKeyCode()) {
/*
* one click => play/pause
* double click => next
*/
case KeyEvent.KEYCODE_HEADSETHOOK:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
long time = SystemClock.uptimeMillis();
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
if (event.getRepeatCount() > 0)
break;
headsetDownTime = time;
break;
case KeyEvent.ACTION_UP:
if (time - headsetUpTime <= 250) {
handler.removeCallbacks(handleSingleClick);
doNext();
} else {
handler.postDelayed(handleSingleClick, 251);
}
headsetUpTime = time;
break;
}
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
doPlay();
break;
case KeyEvent.KEYCODE_MEDIA_PAUSE:
doPause();
break;
case KeyEvent.KEYCODE_MEDIA_STOP:
doStop();
break;
case KeyEvent.KEYCODE_MEDIA_NEXT:
doNext();
break;
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
doPrevious();
break;
}
}
private void registerMediaSessionCallback() {
mediaSession.setCallback(new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
doPlay();
}
@Override
public void onPause() {
doPause();
}
@Override
public void onSkipToNext() {
doNext();
}
@Override
public void onSkipToPrevious() {
doPrevious();
}
@Override
public void onStop() {
doStop();
}
});
}
}