package gilday.android.powerhour.service;
import java.io.IOException;
import java.util.Random;
import gilday.android.powerhour.IDisposable;
import gilday.android.powerhour.MusicUtils;
import gilday.android.powerhour.PowerHourPreferences;
import gilday.android.powerhour.data.PreferenceRepository;
import android.content.ComponentName;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.util.Log;
/**
*
* @author Johnathan Gilday
*
*/
class SongPlayer implements IDisposable {
private static final String TAG = "SongPlayer";
final private Object mplayerLock = new Object();
private MediaPlayer mplayer;
private Context context;
private PreferenceRepository powerHourPrefs;
private AudioFocusStateManager audioFocusStateManager;
/**
* Construct the AudioPlayerManager just before it needs to play songs since it will
* request audio focus from the Android system
* @param powerHourService Needed to determine if the power hour is set to "play"
* @param context
*/
public SongPlayer(Context context, IAudioFocusLostListener audioFocusLostListener) {
mplayer = new MediaPlayer();
this.context = context;
powerHourPrefs = new PreferenceRepository(context);
audioFocusStateManager = new AudioFocusStateManager(context, audioFocusLostListener);
}
/**
* Will reload the MediaPlayer with a new song. Will seek the MediaPlayer to the correct position
* based on settings. If PowerHourService is playing, will play the song after seek operation complete
* @param songId Song to prepare
* @throws IllegalArgumentException
* @throws IllegalStateException
* @throws IOException
*/
public void prepareNextSong(int songId, final ISongPreparedListener callback) throws IllegalArgumentException, IllegalStateException, IOException {
int msOffset = calculateMillisecondOffset(songId);
synchronized(mplayerLock){
mplayer.reset();
// Get next song's path from MusicUtils
String path = MusicUtils.getPathForSong(songId);
// try set and prepare next song
mplayer.setDataSource(path);
mplayer.prepare();
// Set the offset
mplayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
public void onSeekComplete(MediaPlayer mp) {
double offset = powerHourPrefs.getOffset();
Log.d(TAG, "Offset: " + offset + ". Current pos: " + mplayer.getCurrentPosition());
callback.onSongPrepared(SongPlayer.this);
mplayer.setOnSeekCompleteListener(null);
}
});
mplayer.seekTo(msOffset);
}
}
public void pause() {
if(mplayer.isPlaying())
mplayer.pause();
}
public void play() {
audioFocusStateManager.tryGetAudioFocus();
if(!audioFocusStateManager.hasFocus()) {
if(mplayer.isPlaying())
mplayer.pause();
return;
}
if(!mplayer.isPlaying()) mplayer.start();
}
/**
* Release MediaPlayer resources and give up audio focus
*/
@Override
public void dispose() {
synchronized(mplayerLock){
mplayer.stop();
mplayer.release();
mplayer = null;
}
audioFocusStateManager.tryAbandonAudioFocus();
}
/**
* Based on power hour settings, will determine the number of milliseconds to skip
* before starting the media playback.
* @param songId the integer ID of the song. Needed to figure out how long the song is
* @return Media playback starting position in ms
*/
private int calculateMillisecondOffset(int songId) {
int msOffset = 0;
double offset = powerHourPrefs.getOffset();
if(offset != 0){
// Get the duration of this song in ms
long duration = MusicUtils.getDuration(context, songId);
// Calculate offset
// Is offset random
if(offset == PowerHourPreferences.RANDOM){
// Ensure the offset will leave at least a minute of playback
// MusicUtils returns duration in ms so subtract 60 seconds * 1000
double maxOffset = (double)(duration - 60000) / duration;
offset = (new Random().nextDouble() * maxOffset);
}
Log.v("OFFSET", "OFFSET: " + offset);
msOffset = (int) (offset * duration);
//msOffset = (int) (((double)(percent / 100)) * duration);
// Is there enough song left to finish the minute?
if(duration - msOffset < 60000){
// Nope, play the whole song. Don't account for songs
// that are < 60 seconds: If the user put a song < 60
// seconds on a Power Hour they're retarded and deserve to
// sit through silence.
// TODO: Meh. Maybe do something about this. I'm sure voice memos don't want to be heard
// then again, just skip it?
msOffset = 0;
}
}
return msOffset;
}
class AudioFocusStateManager implements OnAudioFocusChangeListener
{
private AudioManager audioManager;
private ComponentName mediaButtonReceiverClass;
private AudioFocusState currentState;
private IAudioFocusLostListener audioFocusLostListener;
private InitialState initialState;
private FocusedState focusedState;
private NoFocusNoDuckState noFocusNoDuckState;
private NoFocusCanDuckState noFocusCanDuckState;
AudioFocusStateManager(Context context, IAudioFocusLostListener audioFocusLostListener) {
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mediaButtonReceiverClass = new ComponentName(context, MediaButtonsReceiver.class);
this.audioFocusLostListener = audioFocusLostListener;
initialState = new InitialState();
focusedState = new FocusedState();
noFocusNoDuckState = new NoFocusNoDuckState();
noFocusCanDuckState = new NoFocusCanDuckState();
currentState = initialState;
}
boolean hasFocus() {
return currentState == focusedState;
}
void tryGetAudioFocus() {
if(currentState != focusedState && AudioManager.AUDIOFOCUS_REQUEST_GRANTED ==
audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN)) {
currentState.onGainingFocus();
currentState = focusedState;
}
}
void tryAbandonAudioFocus() {
if(currentState != noFocusNoDuckState
&& AudioManager.AUDIOFOCUS_REQUEST_GRANTED == audioManager.abandonAudioFocus(this)) {
currentState.onGivingUpFocus();
currentState = noFocusNoDuckState;
}
}
@Override
public void onAudioFocusChange(int focusCode) {
String tag = "SongPlayer";
switch(focusCode){
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
case AudioManager.AUDIOFOCUS_GAIN:
Log.v(tag, "AUDIOFOCUS_GAIN");
if(currentState != focusedState) {
currentState.onGainingFocus();
currentState = focusedState;
}
break;
case AudioManager.AUDIOFOCUS_LOSS:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
if(currentState != noFocusNoDuckState) {
currentState.onRobbedOfFocusNoDuck();
currentState = noFocusNoDuckState;
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
Log.v(tag, "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK");
if(currentState != noFocusCanDuckState) {
// We can duck, just lower the volume for now
currentState.onRobbedOfFocusCanDuck();
currentState = noFocusCanDuckState;
}
break;
}
}
abstract class AudioFocusState
{
private String TAG = "AudioFocus";
void onGainingFocus() {
Log.v(TAG, "Gained Focus");
// Register buttons
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiverClass);
}
/**
* Report indefinite loss of focus
*/
void onRobbedOfFocusNoDuck() {
Log.v(TAG, "Lost Focus No Duck");
audioFocusLostListener.onAudioFocusLost();
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiverClass);
}
/**
* Lower volume
*/
void onRobbedOfFocusCanDuck() {
Log.v(TAG, "Lost Focus Can DucK");
mplayer.setVolume(0.1f, 0.1f);
}
void onGivingUpFocus() {
Log.v(TAG, "Giving up focus");
// Unregister buttons
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiverClass);
}
}
class InitialState extends AudioFocusState { }
class FocusedState extends AudioFocusState {
@Override
void onGainingFocus() { }
}
class NoFocusNoDuckState extends AudioFocusState { }
class NoFocusCanDuckState extends AudioFocusState
{
@Override
void onGainingFocus() {
Log.v(TAG, "Bump volume up");
// Regained focus. Bump volume back up
mplayer.setVolume(1.0f, 1.0f);
}
}
}
}