/*
* Copyright 2016 Substance Mobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.animbus.music.media.stable;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.PowerManager;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import com.animbus.music.media.objects.Song;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static android.media.AudioManager.OnAudioFocusChangeListener;
import static android.media.MediaPlayer.OnCompletionListener;
import static android.media.MediaPlayer.OnErrorListener;
import static android.media.MediaPlayer.OnPreparedListener;
import static android.media.MediaPlayer.OnSeekCompleteListener;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_FAST_FORWARDING;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_NONE;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_PAUSED;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_PLAYING;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_REWINDING;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_STOPPED;
/**
* WARNING: This class is extremely experimental. This file was created on 7/20/2015 by Adrian.
*/
public class PlaybackManager implements OnAudioFocusChangeListener, OnPreparedListener, OnErrorListener, OnCompletionListener, OnSeekCompleteListener {
/**
* The volume we set the media player to when GEM loses audio focus, but is allowed to reduce the volume instead of stopping playback.
*/
public static final float VOLUME_DUCK = 0.2f;
/**
* The volume we set the media player when GEM has audio focus.
*/
public static final float VOLUME_NORMAL = 1.0f;
/**
* This is the value you would use for tis situation:
* The user presses "Previous". if the current position in the song is greater then this value,
* pressing "Previous" will restart the song, if not it will play the previous song
*/
public static final int MAX_DURATION_FOR_REPEAT = 3000;
private final static PlaybackManager instance = new PlaybackManager();
/**
* This clsss's log tag
*/
private static final String TAG = "Playback Manager:";
/**
* GEM doesn't have audio focus, and can't duck (play at a low volume)
*/
private static final int AUDIO_NO_FOCUS_NO_DUCK = 0;
/**
* GEM doesn't have focus, but can duck (play at a low volume)
*/
private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1;
/**
* GEM has full audio focus
*/
private static final int AUDIO_FOCUSED = 2;
public Song mSong;
private MediaService mService;
/**
* Type of audio focus GEM currently has:
*/
private int mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK;
/**
* Underlying Media Player
*/
private MediaPlayer mMediaPlayer;
/**
* AudioManager used to change volume and manage audio focus
*/
private AudioManager mAudioManager;
private boolean mPlayOnFocusGain;
/**
* The ID of the current song
*/
private volatile long mCurrentMediaId;
public Context mContext;
private volatile int mCurrentPosition;
public ArrayList<OnChangedListener> listeners = new ArrayList<>();
public MediaNotification mNotification;
private MediaService ervice;
///////////////////////////////////////////////////////////////////////////
// Ways to get the class...
///////////////////////////////////////////////////////////////////////////
private PlaybackManager() {
}
public static PlaybackManager get() {
return instance;
}
public static PlaybackManager from(MediaService service) {
return PlaybackManager.get().setContext(service).setService(service);
}
public PlaybackManager setContext(Context context) {
this.mContext = context;
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
return this;
}
public PlaybackManager setService(MediaService service) {
mService = service;
return this;
}
///////////////////////////////////////////////////////////////////////////
// Controls
///////////////////////////////////////////////////////////////////////////
/**
* This is the play method that plays music from an item
*
* @param item this is for the player to recognise what song to play
*/
public void play(Song item) {
mPlayOnFocusGain = true;
tryToGetAudioFocus();
long mediaId = item.getID();
boolean mediaHasChanged = !(mediaId == mCurrentMediaId);
if (mediaHasChanged) {
mCurrentPosition = 0;
mCurrentMediaId = mediaId;
}
if (mService.getState() == STATE_PAUSED && !mediaHasChanged && mMediaPlayer != null) {
configMediaPlayerState();
} else {
mService.setState(STATE_STOPPED);
relaxResources(false); // release everything except MediaPlayer
try {
createMediaPlayerIfNeeded();
mService.setState(STATE_PLAYING);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setDataSource(mContext, item.getUri());
// Starts preparing the media player in the background. When
// it's done, it will call our OnPreparedListener (that is,
// the onPrepared() method on this class, since we set the
// listener to 'this'). Until the media player is prepared,
// we *cannot* call start() on it!
mMediaPlayer.prepareAsync();
mSong = item;
for (OnChangedListener l : listeners) {
l.onSongChanged(item);
}
mService.setAsForeground();
} catch (IOException ex) {
Log.e(TAG, "Exception playing song", ex);
}
}
}
/**
* A variaton of play that uses an item extracted from a list
*/
public void play(List<Song> list, int songPos) {
ArrayList<Song> data = new ArrayList<>(list);
play(data.get(songPos));
QueueManager.get().setQueue(data);
QueueManager.get().setCurrentSongPos(songPos);
}
public void playQueueItem(int pos) {
play(QueueManager.get().getCurrentQueueAsSong().get(pos));
QueueManager.get().setCurrentSongPos(pos);
}
/**
* Plays the next song in the queue
*/
public void playNext() {
play(QueueManager.get().getCurrentQueueAsSong(), QueueManager.get().updateNextSongPos());
}
/**
* Plays the previous song in the queue
*/
public void playPrev(boolean processSongPos) {
if (processSongPos) {
if (mMediaPlayer.getCurrentPosition() > MAX_DURATION_FOR_REPEAT) {
play(QueueManager.get().getCurrentQueueAsSong(), QueueManager.get().getCurrentSongPos());
mCurrentPosition = 0;
} else {
play(QueueManager.get().getCurrentQueueAsSong(), QueueManager.get().updatePrevSongPos());
}
} else {
play(QueueManager.get().getCurrentQueueAsSong(), QueueManager.get().updatePrevSongPos());
}
}
/**
* Pauses the playback
*/
public void pause() {
if (mService.getState() == STATE_PLAYING) {
// Pause media player and cancel the 'foreground service' state.
if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
mCurrentPosition = mMediaPlayer.getCurrentPosition();
}
// while paused, retain the MediaPlayer but give up audio focus
relaxResources(false);
giveUpAudioFocus();
mService.removeForeground(false);
}
mService.setState(STATE_PAUSED);
}
/**
* Resumes playback if it is paused
*/
public void resume() {
if (mService.getState() == STATE_PAUSED && mMediaPlayer != null) {
mMediaPlayer.start();
mService.setState(STATE_PLAYING);
tryToGetAudioFocus();
mService.setAsForeground();
Log.d(TAG, "Resuming");
} else {
Log.d(TAG, "Not paused or MediaPlayer is null. Player is null: " + (mMediaPlayer == null));
}
}
/**
* This is what is called to stop playback
*/
public void stop() {
mService.setState(STATE_STOPPED);
mCurrentPosition = getCurrentPosInSong();
// Give up Audio focus
giveUpAudioFocus();
// Relax all resources
relaxResources(true);
//Stop the service
mService.removeForeground(true);
mService.stopService();
}
public void seekTo(int position) {
Log.d(TAG, "seekTo called with " + position);
if (mMediaPlayer == null) {
// If we do not have a current media player, simply update the current position
mCurrentPosition = position;
} else {
if (getCurrentPosInSong() > position) {
//If the old position is greater then the new one, then you are rewinding
mService.setState(STATE_REWINDING);
} else {
//If the old one is less then the new one, then you are fast forwarding
mService.setState(STATE_FAST_FORWARDING);
}
mCurrentPosition = position;
mMediaPlayer.seekTo(position);
}
}
public void setVolume(int mediaVolume) {
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mediaVolume, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
public void setRepeat(boolean repeat) {
mMediaPlayer.setLooping(repeat);
}
///////////////////////////////////////////////////////////////////////////
// Interfaces
///////////////////////////////////////////////////////////////////////////
/**
* Called by AudioManager on audio focus changes.
* Implementation of {@link android.media.AudioManager.OnAudioFocusChangeListener}
*/
@Override
public void onAudioFocusChange(int focusChange) {
Log.d(TAG, "onAudioFocusChange. focusChange=" + focusChange);
if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// We have gained focus:
mAudioFocus = AUDIO_FOCUSED;
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS ||
focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// We have lost focus. If we can duck (low playback volume), we can keep playing.
// Otherwise, we need to pause the playback.
boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
mAudioFocus = canDuck ? AUDIO_NO_FOCUS_CAN_DUCK : AUDIO_NO_FOCUS_NO_DUCK;
// If we are playing, we need to reset media player by calling configMediaPlayerState
// with mAudioFocus properly set.
if (mService.getState() == STATE_PLAYING && !canDuck) {
// If we don't have audio focus and can't duck, we save the information that
// we were playing, so that we can resume playback once we get the focus back.
//todo setting
mPlayOnFocusGain = true;
}
} else {
Log.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange);
}
configMediaPlayerState();
}
/**
* Called when MediaPlayer has completed a seek
*
* @see android.media.MediaPlayer.OnSeekCompleteListener
*/
@Override
public void onSeekComplete(MediaPlayer mp) {
Log.d(TAG, "onSeekComplete from MediaPlayer:" + mp.getCurrentPosition());
mCurrentPosition = mp.getCurrentPosition();
if (mService.getState() == STATE_REWINDING || mService.getState() == STATE_FAST_FORWARDING) {
mMediaPlayer.start();
mService.setState(STATE_PLAYING);
}
}
/**
* Called when media player is done playing current song.
*
* @see android.media.MediaPlayer.OnCompletionListener
*/
@Override
public void onCompletion(MediaPlayer player) {
Log.d(TAG, "onCompletion from MediaPlayer");
if (!player.isLooping()) {
// The media player finished playing the current song, so we go ahead and start the next.
play(QueueManager.get().getCurrentQueueAsSong(), QueueManager.get().updateNextSongPos());
}
}
/**
* Called when media player is done preparing.
*
* @see android.media.MediaPlayer.OnPreparedListener
*/
@Override
public void onPrepared(MediaPlayer player) {
Log.d(TAG, "onPrepared from MediaPlayer");
// The media player is done preparing. That means we can start playing if we
// have audio focus.
configMediaPlayerState();
}
/**
* Called when there's an error playing media. When this happens, the media
* player goes to the Error state. We warn the user about the error and
* reset the media player.
*
* @see android.media.MediaPlayer.OnErrorListener
*/
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Log.e(TAG, "Media player error: what=" + what + ", extra=" + extra);
return true; // true indicates we handled the error
}
public MediaPlayer getMediaPlayer() {
return mMediaPlayer;
}
///////////////////////////////////////////////////////////////////////////
// Change Listener
///////////////////////////////////////////////////////////////////////////
public interface OnChangedListener {
void onSongChanged(Song song);
void onPlaybackStateChanged(PlaybackStateCompat state);
}
public void registerListener(OnChangedListener l) {
listeners.add(l);
}
public void unregisterListener(OnChangedListener l) {
listeners.remove(l);
}
///////////////////////////////////////////////////////////////////////////
// Meat and potatoes of the code
///////////////////////////////////////////////////////////////////////////
/**
* Try to get the system audio focus.
*/
private void tryToGetAudioFocus() {
Log.d(TAG, "tryToGetAudioFocus");
if (mAudioFocus != AUDIO_FOCUSED) {
int result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mAudioFocus = AUDIO_FOCUSED;
}
}
}
/**
* Give up the audio focus.
*/
private void giveUpAudioFocus() {
Log.d(TAG, "giveUpAudioFocus");
if (mAudioFocus == AUDIO_FOCUSED) {
if (mAudioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mAudioFocus = AUDIO_NO_FOCUS_NO_DUCK;
}
}
}
/**
* Reconfigures MediaPlayer according to audio focus settings and
* starts/restarts it. This method starts/restarts the MediaPlayer
* respecting the current audio focus state. So if we have focus, it will
* play normally; if we don't have focus, it will either leave the
* MediaPlayer paused or set it to a low volume, depending on what is
* allowed by the current focus settings. This method assumes mPlayer !=
* null, so if you are calling it, you have to do so from a context where
* you are sure this is the case.
*/
private void configMediaPlayerState() {
Log.d(TAG, "configMediaPlayerState. mAudioFocus=" + mAudioFocus);
if (mAudioFocus == AUDIO_NO_FOCUS_NO_DUCK) {
// If we don't have audio focus and can't duck, we have to pause,
if (mService.getState() == STATE_PLAYING) {
pause();
}
} else { // we have audio focus:
if (mAudioFocus == AUDIO_NO_FOCUS_CAN_DUCK) {
mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet
} else {
if (mMediaPlayer != null) {
mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again
} // else do something for remote client.
}
// If we were playing when we lost focus, we need to resume playing.
if (mPlayOnFocusGain) {
if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
Log.d(TAG, "configMediaPlayerState startMediaPlayer. seeking to " + mCurrentPosition);
if (mCurrentPosition == mMediaPlayer.getCurrentPosition()) {
mMediaPlayer.start();
mService.setState(STATE_PLAYING);
} else {
mMediaPlayer.seekTo(mCurrentPosition);
}
}
mPlayOnFocusGain = false;
}
}
}
/**
* Makes sure the media player exists and has been reset. This will create
* the media player if needed, or reset the existing media player if one
* already exists.
*/
private void createMediaPlayerIfNeeded() {
Log.d(TAG, "createMediaPlayerIfNeeded. Needed: " + (mMediaPlayer == null));
if (mMediaPlayer == null) {
mMediaPlayer = new MediaPlayer();
// Make sure the media player will acquire a wake-lock while
// playing. If we don't do that, the CPU might go to sleep while the
// song is playing, causing playback to stop.
mMediaPlayer.setWakeMode(mContext,
PowerManager.PARTIAL_WAKE_LOCK);
// we want the media player to notify us when it's ready preparing,
// and when it's done playing:
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.setOnCompletionListener(this);
mMediaPlayer.setOnErrorListener(this);
mMediaPlayer.setOnSeekCompleteListener(this);
} else {
mMediaPlayer.reset();
}
}
/**
* Releases resources used by the service for playback. This includes the
* "foreground service" status, the wake locks and possibly the MediaPlayer.
*
* @param releaseMediaPlayer Indicates whether the Media Player should also
* be released or not
*/
private void relaxResources(boolean releaseMediaPlayer) {
Log.d(TAG, "RelaxResources. ReleaseMediaPlayer=" + releaseMediaPlayer);
mService.removeForeground(false);
// stop and release the Media Player, if it's available
if (releaseMediaPlayer && mMediaPlayer != null) {
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
///////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////
/**
* When called, this tells the app wheather the app is playing music
*/
public boolean isPlaying() {
return mPlayOnFocusGain || (mMediaPlayer != null && mMediaPlayer.isPlaying());
}
public int getCurrentPosInSong() {
return mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : mCurrentPosition;
}
public Song getCurrentSong() {
return mSong;
}
public boolean isActive() {
return mService != null && mService.getSession() != null && !(mService.getState() == STATE_STOPPED || mService.getState() == STATE_NONE) && mService.getSession().isActive();
}
public MediaService getService() {
return mService;
}
public boolean isLooping() {
return mMediaPlayer.isLooping();
}
public boolean isInitiated() {
return mService != null;
}
}