/*
* Copyright (C) 2017 Team Gateship-One
* (Hendrik Borghorst & Frederik Luetkes)
*
* The AUTHORS.md file contains a detailed contributors list:
* <https://github.com/gateship-one/odyssey/blob/master/AUTHORS.md>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.gateshipone.odyssey.playbackservice;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Random;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.Process;
import android.util.Log;
import android.widget.Toast;
import org.gateshipone.odyssey.artworkdatabase.ArtworkManager;
import org.gateshipone.odyssey.models.FileModel;
import org.gateshipone.odyssey.models.TrackModel;
import org.gateshipone.odyssey.playbackservice.managers.PlaybackServiceStatusHelper;
import org.gateshipone.odyssey.playbackservice.statemanager.OdysseyDatabaseManager;
import org.gateshipone.odyssey.utils.FileExplorerHelper;
import org.gateshipone.odyssey.utils.MusicLibraryHelper;
public class PlaybackService extends Service implements AudioManager.OnAudioFocusChangeListener {
/**
* enums for random, repeat state
*/
public enum RANDOMSTATE {
// If random mode is off
RANDOM_OFF,
// If random mode is on
RANDOM_ON
}
public enum REPEATSTATE {
// If repeat mode is off
REPEAT_OFF,
// If the playlist should be repeated
REPEAT_ALL,
// If the current track should be repeated
REPEAT_TRACK
}
/**
* PlaybackState enum for the PlaybackService
*/
public enum PLAYSTATE {
// If a song is actual playing
PLAYING,
// If a song was playing before but is now paused (but the GaplessPlayer is prepared and ready to resume).
PAUSE,
// If the PBS was loaded and is ready to resume playing. This is a state
// where the user never actually played a song in this session (GaplessPlayer is not yet prepared).
RESUMED,
// If no track is in the playlist the state is stopped. (Does not mean the PBS is not running)
STOPPED
}
/**
* Idle state of this service. Used for notifying the user in the GUI about long running activites
*/
public enum PLAYBACKSERVICESTATE {
// If the service is performing an operation
WORKING,
// If the service is finished with the operation
IDLE
}
/**
* Tag used for debugging
*/
private static final String TAG = "OdysseyPlaybackService";
private static final String HANDLER_THREAD_NAME = "OdysseyPBSHandler";
/**
* Constants for Intent actions
*/
public static final String ACTION_PLAY = "org.gateshipone.odyssey.play";
public static final String ACTION_PAUSE = "org.gateshipone.odyssey.pause";
public static final String ACTION_NEXT = "org.gateshipone.odyssey.next";
public static final String ACTION_PREVIOUS = "org.gateshipone.odyssey.previous";
public static final String ACTION_SEEKTO = "org.gateshipone.odyssey.seekto";
public static final String ACTION_STOP = "org.gateshipone.odyssey.stop";
public static final String ACTION_QUIT = "org.gateshipone.odyssey.quit";
public static final String ACTION_TOGGLEPAUSE = "org.gateshipone.odyssey.togglepause";
/**
* Request code for the timeout intent when the PlaybackService is waiting to quit
*/
private static final int TIMEOUT_INTENT_QUIT_REQUEST_CODE = 5;
/**
* Timeout time that the PlaybackService waits until it stops itself in milliseconds. (5 Minutes)
*/
private final static int SERVICE_CANCEL_TIME = 5 * 60 * 1000;
/**
* Defines how often the random generator should retry to get a random number if it always
* hits the currently running track
*/
private final static int RANDOM_RETRIES = 20;
/**
* Thread in which messages are handled
*/
private HandlerThread mHandlerThread;
/**
* Handler that executes action requested by a message
*/
private PlaybackServiceHandler mHandler;
/**
* Saves if the audiofocus was lost for some reason. If it is set the playback will resume,
* when the audiofocus gets back to this class.
*/
private boolean mLostAudioFocus = false;
/**
* GaplessPlayer object that handles all the MediaPlayer objects and encapsulates the gapless
* functions of android.
*/
private GaplessPlayer mPlayer;
/**
* Currently active playlist.
*/
private List<TrackModel> mCurrentList;
/**
* Index of the currently active track.
*/
private int mCurrentPlayingIndex;
/**
* Index of the track that is played next. Does not necessarily be the mCurrentPlayingIndex + 1
* because random could be activated.
*/
private int mNextPlayingIndex;
/**
* Saves the index of the track that was played before the current one.
*/
private int mLastPlayingIndex;
/**
* Saves if the volume is temporarily reduced because of a notification (for example)
*/
private boolean mIsDucked = false;
/**
* Saves the time (in milliseconds) of the last playback.
*/
private int mLastPosition = 0;
/**
* Random generator used to find a random position when random playback is active.
*/
private Random mRandomGenerator;
/**
* Saves if random playback is active
*/
private RANDOMSTATE mRandom = RANDOMSTATE.RANDOM_OFF;
/**
* Saves if repeat is active
*/
private REPEATSTATE mRepeat = REPEATSTATE.REPEAT_OFF;
/**
* MediaControls manager
*/
private PlaybackServiceStatusHelper mPlaybackServiceStatusHelper;
/**
* Temporary wakelock for transition to next song.
* Without it, some android devices go to sleep and don't start
* the next song.
*/
private WakeLock mSongTransitionWakelock = null;
/**
* Databasemanager for saving and restoring states including their playlist
*/
private OdysseyDatabaseManager mDatabaseManager = null;
/**
* BroadcastReceiver that handles all control intents
*/
private BroadcastControlReceiver mBroadcastControlReceiver = null;
private boolean mBusy = false;
/**
* Called when the PlaybackService is bound by an activity.
*
* @param intent Intent used for binding.
* @return The Interface used for the service connection.
*/
@Override
public IBinder onBind(Intent intent) {
return new OdysseyPlaybackServiceInterface(this);
}
/**
* Called when an activity unbounds from this service.
*
* @param intent Intent used for unbinding
* @return True if unbound successfully
*/
@Override
public boolean onUnbind(final Intent intent) {
super.onUnbind(intent);
return true;
}
/**
* Called when the service is created because it is requested by an activity
*/
@Override
public void onCreate() {
super.onCreate();
// Start Handlerthread which is used for the asynchronous handler.
mHandlerThread = new HandlerThread(HANDLER_THREAD_NAME, Process.THREAD_PRIORITY_DEFAULT);
mHandlerThread.start();
mHandler = new PlaybackServiceHandler(mHandlerThread.getLooper(), this);
// Create MediaPlayer object used throughout the complete runtime of this service
mPlayer = new GaplessPlayer(this);
// Register listeners with the GaplessPlayer
mPlayer.setOnTrackStartListener(new PlaybackStartListener());
mPlayer.setOnTrackFinishedListener(new PlaybackFinishListener());
// Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// set up the OdysseyDatabaseManager
mDatabaseManager = new OdysseyDatabaseManager(getApplicationContext());
// read a possible saved playlist from the database
mCurrentList = mDatabaseManager.readPlaylist();
// read a possible saved state from database
OdysseyServiceState state = mDatabaseManager.getState();
// Resume the loaded state to internal variables
mCurrentPlayingIndex = state.mTrackNumber;
mLastPosition = state.mTrackPosition;
mRandom = state.mRandomState;
mRepeat = state.mRepeatState;
// Check if saved state is within bounds of resumed playlist
if (mCurrentPlayingIndex < 0 || mCurrentPlayingIndex > mCurrentList.size()) {
mCurrentPlayingIndex = -1;
}
// Internal state initialization
mLastPlayingIndex = -1;
mNextPlayingIndex = -1;
// Create a new BroadcastControlReceiver that handles all control broadcasts sent to the PlaybackService
if (mBroadcastControlReceiver == null) {
// Create a new instance if not already done, FIXME is this actual possible?
mBroadcastControlReceiver = new BroadcastControlReceiver();
// Create a filter to only handle certain actions
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY);
intentFilter.addAction(ACTION_PREVIOUS);
intentFilter.addAction(ACTION_PAUSE);
intentFilter.addAction(ACTION_PLAY);
intentFilter.addAction(ACTION_TOGGLEPAUSE);
intentFilter.addAction(ACTION_NEXT);
intentFilter.addAction(ACTION_STOP);
intentFilter.addAction(ACTION_QUIT);
intentFilter.addAction(ArtworkManager.ACTION_NEW_ARTWORK_READY);
// Register the receiver within the system
registerReceiver(mBroadcastControlReceiver, intentFilter);
}
// Request the powermanager to initialize the transition wakelock
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
// Initialize the transition wake lock (see above for the reason)
mSongTransitionWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
// set up random generator used for random playback
mRandomGenerator = new Random();
// Initialize the mediacontrol manager for lockscreen pictures and remote control
mPlaybackServiceStatusHelper = new PlaybackServiceStatusHelper(this);
}
/**
* Called when an intent is used to start the service (e.g. from the widget)
*
* @param intent Intent used for starting the PlaybackService
* @param flags Some flags (not used)
* @param startId Id (not used)
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
super.onStartCommand(intent, flags, startId);
if (null != intent && intent.getExtras() != null) {
String action = intent.getExtras().getString("action");
if (action != null) {
switch (action) {
case ACTION_TOGGLEPAUSE:
togglePause();
break;
case ACTION_NEXT:
setNextTrack();
break;
case ACTION_PREVIOUS:
setPreviousTrack();
break;
case ACTION_STOP:
stop();
break;
case ACTION_PLAY:
resume();
break;
case ACTION_QUIT:
stopService();
break;
}
}
} else {
return START_NOT_STICKY;
}
return START_STICKY;
}
/**
* Called when the system wants to get rid of Odyssey :(. Clean up and unregister BroadcastReceivers
*/
@Override
public void onDestroy() {
// Cancel any pending quit alerts
cancelQuitAlert();
// Unregister a existing broadcastreceiver
if (mBroadcastControlReceiver != null) {
unregisterReceiver(mBroadcastControlReceiver);
mBroadcastControlReceiver = null;
}
// Stop myself
stopSelf();
}
/**
* Directly plays uri
*/
public void playURI(TrackModel track) {
// Clear playlist, enqueue uri, jumpto 0
clearPlaylist();
enqueueTrack(track);
jumpToIndex(0);
}
/**
* Cancels possible outstanding alerts registered within the AlarmManager to quit this service.
*/
public synchronized void cancelQuitAlert() {
// Request the alarm manager and quit the alert with the given TIMEOUT_INTENT_QUIT_REQUEST_CODE
AlarmManager am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
Intent quitIntent = new Intent(ACTION_QUIT);
PendingIntent quitPI = PendingIntent.getBroadcast(this, TIMEOUT_INTENT_QUIT_REQUEST_CODE, quitIntent, PendingIntent.FLAG_UPDATE_CURRENT);
am.cancel(quitPI);
}
/**
* Stops all playback and the service afterwards, because it usually is not required afterwards
*/
public void stop() {
if (mCurrentList.size() > 0 && mCurrentPlayingIndex >= 0 && (mCurrentPlayingIndex < mCurrentList.size())) {
// Notify simple last.fm scrobbler about playback stop
mPlaybackServiceStatusHelper.notifyLastFM(mCurrentList.get(mCurrentPlayingIndex), PlaybackServiceStatusHelper.SLS_STATES.SLS_COMPLETE);
}
// Request the GaplessPlayer to stop its playback.
mPlayer.stop();
// Reset the interal state variables
mCurrentPlayingIndex = 0;
mLastPosition = 0;
mNextPlayingIndex = -1;
mLastPlayingIndex = -1;
// Request to stop the service
stopService();
}
/**
* Pauses playback (if one is running) otherwise is doing nothing.
*/
public void pause() {
// Check if GaplessPlayer is playing something
if (mPlayer.isRunning()) {
// Pause the playback before saving the position
mPlayer.pause();
// Save the position because it is later used to save the state in the database
mLastPosition = mPlayer.getPosition();
// Start an alert within the AlarmManager to quit this service after a timeout (defined in SERVICE_CANCEL_TIME)
AlarmManager am = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
Intent quitIntent = new Intent(ACTION_QUIT);
PendingIntent quitPI = PendingIntent.getBroadcast(this, TIMEOUT_INTENT_QUIT_REQUEST_CODE, quitIntent, PendingIntent.FLAG_UPDATE_CURRENT);
am.set(AlarmManager.RTC, System.currentTimeMillis() + SERVICE_CANCEL_TIME, quitPI);
// Broadcast simple.last.fm.scrobble broadcast to inform about pause state
if (mCurrentPlayingIndex >= 0 && (mCurrentPlayingIndex < mCurrentList.size())) {
TrackModel item = mCurrentList.get(mCurrentPlayingIndex);
}
}
// Distribute the new status to everything (Notification, widget, ...)
mPlaybackServiceStatusHelper.updateStatus();
}
/**
* Resumes playback of a previously paused playback. If called first after starting the service,
* this will resume the last state (loaded from the database)
*/
public void resume() {
cancelQuitAlert();
// Check if mediaplayer needs preparing because we are resuming an state from the database or stopped state
if (!mPlayer.isPrepared() && (mCurrentPlayingIndex != -1) && (mCurrentPlayingIndex < mCurrentList.size())) {
jumpToIndex(mCurrentPlayingIndex, mLastPosition);
return;
}
// Check if no mCurrentPlayingIndex is available which means that we should start playing position 0 (if available).
if (mCurrentPlayingIndex < 0 && mCurrentList.size() > 0) {
// Songs exist, so start playback of playlist begin
jumpToIndex(0);
} else if (mCurrentPlayingIndex < 0 && mCurrentList.size() == 0) {
// If no songs are enqueued to the playlist just do nothing here. FIXME is this update necessary? (no change in state)
// mPlaybackServiceStatusHelper.updateStatus();
} else if (mCurrentPlayingIndex < mCurrentList.size()) {
/*
* Make sure service is "started" so android doesn't handle it as a
* "bound service"
*/
Intent serviceStartIntent = new Intent(this, PlaybackService.class);
serviceStartIntent.addFlags(Intent.FLAG_FROM_BACKGROUND);
startService(serviceStartIntent);
// Request audio focus before doing anything
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// Abort command if we don't acquired the audio focus
return;
}
// Notify the helper class to start a media session
mPlaybackServiceStatusHelper.startMediaSession();
// This will instruct the GaplessPlayer to actually start playing
mPlayer.resume();
// Notify simple last.fm scrobbler about the playback resume
mPlaybackServiceStatusHelper.notifyLastFM(mCurrentList.get(mCurrentPlayingIndex), PlaybackServiceStatusHelper.SLS_STATES.SLS_RESUME);
// Reset the time position because it is invalid now
mLastPosition = 0;
// Notify the helper class, that the state has changed
mPlaybackServiceStatusHelper.updateStatus();
}
}
/**
* Toggles between playing & paused
*/
public void togglePause() {
// Toggles playback state
if (mPlayer.isRunning()) {
pause();
} else {
resume();
}
}
/**
* Add all tracks to the playlist and start playing them
*/
public void playAllTracks() {
// Notify the user about the possible long running operation
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
// clear the playlist before adding all tracks
clearPlaylist();
// Get a list of all available tracks from the MusicLibraryHelper
List<TrackModel> allTracks = MusicLibraryHelper.getAllTracks(getApplicationContext());
mCurrentList.addAll(allTracks);
// Start playing the first item in the list
jumpToIndex(0);
// Notify the user that the operation is now finished
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* Adds all tracks to playlist, play and then shuffle
*/
public void playAllTracksShuffled() {
playAllTracks();
shufflePlaylist();
}
/**
* Shuffles the current playlist
*/
public void shufflePlaylist() {
if (mCurrentList.size() > 0 && mCurrentPlayingIndex >= 0 && (mCurrentPlayingIndex < mCurrentList.size())) {
// get the current TrackModel and remove it from playlist
TrackModel currentItem = mCurrentList.get(mCurrentPlayingIndex);
mCurrentList.remove(mCurrentPlayingIndex);
// shuffle playlist and set currentitem as first element
Collections.shuffle(mCurrentList);
mCurrentList.add(0, currentItem);
// reset index
mCurrentPlayingIndex = 0;
mPlaybackServiceStatusHelper.updateStatus();
// set next track for the GaplessPlayer which has now changed
try {
if (mCurrentPlayingIndex + 1 < mCurrentList.size()) {
mPlayer.setNextTrack(mCurrentList.get(mCurrentPlayingIndex + 1).getTrackURL());
} else {
mPlayer.setNextTrack(null);
}
} catch (GaplessPlayer.PlaybackException e) {
handlePlaybackException(e);
}
} else if (mCurrentList.size() > 0 && mCurrentPlayingIndex < 0) {
// service stopped just shuffle playlist
Collections.shuffle(mCurrentList);
// sent broadcast
mPlaybackServiceStatusHelper.updateStatus();
}
}
/**
* Jumps to the next song that is set in mNextPlayingIndex
*/
public void setNextTrack() {
// Keep device at least for 5 seconds turned on
mSongTransitionWakelock.acquire(5000);
// Save the last playing index, to allow the user to jump back
mLastPlayingIndex = mCurrentPlayingIndex;
// Jump to the mNextPlayingIndex
jumpToIndex(mNextPlayingIndex);
}
/**
* Sets nextplayback track to preceding on in playlist
*/
public void setPreviousTrack() {
// Keep device at least for 5 seconds turned on
mSongTransitionWakelock.acquire(5000);
// Logic to restart the song if playback is not progressed beyond 2000ms.
// This enables the behavior of CD-players which a user is used to.
if (getTrackPosition() > 2000) {
// Check if current song should be restarted
jumpToIndex(mCurrentPlayingIndex);
} else if (mRandom == RANDOMSTATE.RANDOM_ON) {
// handle random mode
if (mLastPlayingIndex == -1) {
// if no mLastPlayingIndex reuse mCurrentPlayingIndex and restart the song
jumpToIndex(mCurrentPlayingIndex);
} else if (mLastPlayingIndex >= 0 && mLastPlayingIndex < mCurrentList.size()) {
// If a song was played before this one, jump back to it
jumpToIndex(mLastPlayingIndex);
}
} else {
// Check if the repeat track mode is activated which means that the user is stuck to the current song
if (mRepeat == REPEATSTATE.REPEAT_TRACK) {
// repeat the current track again
jumpToIndex(mCurrentPlayingIndex);
} else {
// Check if the first playlist element is reached
if ((mCurrentPlayingIndex - 1 >= 0) && mCurrentPlayingIndex < mCurrentList.size() && mCurrentPlayingIndex >= 0) {
// Jump to the previous song (sequential back jump)
jumpToIndex(mCurrentPlayingIndex - 1);
} else if (mRepeat == REPEATSTATE.REPEAT_ALL) {
// In repeat mode next track is last track of playlist
jumpToIndex(mCurrentList.size() - 1);
} else {
// If repeat all is not activated just stop playback if we try to move to position -1
stop();
}
}
}
}
/**
* Getter for the handler used by the service interface
*
* @return The handler of this service
*/
protected PlaybackServiceHandler getHandler() {
return mHandler;
}
/**
* Returns a reference to the playlist. DO NOT MODIFY THE LIST!!!
*
* @return Reference to mCurrentList
*/
public List<TrackModel> getCurrentList() {
return mCurrentList;
}
/**
* @return Size of the playlist
*/
public int getPlaylistSize() {
return mCurrentList.size();
}
/**
* Getter to retrieve TrackModel items from the playlist
*
* @param index Position of the track to return
* @return Valid track if position within bounds, empty track otherwise
*/
public TrackModel getPlaylistTrack(int index) {
if ((index >= 0) && (index < mCurrentList.size())) {
return mCurrentList.get(index);
}
return new TrackModel();
}
/**
* Clears the current playlist and stops playback afterwards. Also resets repeat, random state
*/
public void clearPlaylist() {
// Clear the list
mCurrentList.clear();
// reset random and repeat state
mRandom = RANDOMSTATE.RANDOM_OFF;
mRepeat = REPEATSTATE.REPEAT_OFF;
// Stop the playback
stop();
}
/**
* Jumps playback to the given index
*
* @param index Position of song to play
*/
public void jumpToIndex(int index) {
jumpToIndex(index, 0);
}
/**
* Jumps playback to the given index and position inside the song (in milliseconds)
*
* @param index Position of the song to play
* @param jumpTime Position inside the song (milliseconds)
*/
public void jumpToIndex(int index, int jumpTime) {
// Prevent that the user freaks out and taps one song after another. This waits for finishing, to prevent race conditions.
if (mPlayer.getActive()) {
// Abort here if the player is still active
return;
}
// Cancel possible alerts registered within the AlarmManager
cancelQuitAlert();
// Stop playback before starting a new song. This ensures state safety
mPlayer.stop();
// Set mCurrentPlayingIndex to new song after checking the bounds
if (index < mCurrentList.size() && index >= 0) {
mCurrentPlayingIndex = index;
/*
* Make sure service is "started" so android doesn't handle it as a
* "bound service"
*/
Intent serviceStartIntent = new Intent(this, PlaybackService.class);
serviceStartIntent.addFlags(Intent.FLAG_FROM_BACKGROUND);
startService(serviceStartIntent);
// Get the item that is requested to be played.
TrackModel item = mCurrentList.get(mCurrentPlayingIndex);
// Request audio focus before doing anything
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// Abort command if audio focus was not granted
return;
}
// Notify the PlaybackServiceStatusHelper that a new media session is started
mPlaybackServiceStatusHelper.startMediaSession();
// Try to start playback of the track url.
try {
mPlayer.play(item.getTrackURL(), jumpTime);
} catch (GaplessPlayer.PlaybackException e) {
// Handle an error of the play commant
handlePlaybackException(e);
}
// Sets the mNextPlayingIndex to the index just startet, because the PlaybackStartListener will
// set the mCurrentPlayingIndex to the mNextPlayingIndex. This ensures that no additional code
// is necessary to handle playback start
mNextPlayingIndex = index;
} else if (index < 0) {
// If index < 0 is requested for whatever reason stop the playback
stop();
}
}
/**
* Seeks the GaplessPlayer to the given position, but only if it is playing.
*
* @param position Position in milliseconds to seek to
*/
public void seekTo(int position) {
if (mPlayer.isRunning()) {
mPlayer.seekTo(position);
}
}
/**
* Returns the playback position of the GaplessPlayer
*
* @return Playback position if playing/paused or 0 if stopped
*/
public int getTrackPosition() {
switch (getPlaybackState()) {
case PLAYING:
return mPlayer.getPosition();
case PAUSE:
case RESUMED:
return mLastPosition;
case STOPPED:
return 0;
}
return 0;
}
/**
* @return Returns the duration of the active track or 0 if no active track
*/
public int getTrackDuration() {
return mPlayer.getDuration();
}
/**
* Enqueue all given tracks.
* Prepare the next track for playback if needed.
*/
public void enqueueTracks(List<TrackModel> tracklist) {
// Saved to check if we played the last song of the list
int oldSize = mCurrentList.size();
// Add the tracks to the actual list
mCurrentList.addAll(tracklist);
/*
* If currently playing and playing is the last one in old playlist set
* enqueued one to next one for gapless mediaplayback
*/
if (mCurrentPlayingIndex == (oldSize - 1) && (mCurrentPlayingIndex >= 0)) {
// Next song for MP has to be set for gapless mediaplayback
mNextPlayingIndex = mCurrentPlayingIndex + 1;
setNextTrackForMP();
}
// Inform the helper that the state has changed
mPlaybackServiceStatusHelper.updateStatus();
}
/**
* Enqueue all tracks of an album identified by the albumKey.
*
* @param albumKey The key of the album
*/
public void enqueueAlbum(String albumKey) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
// get all tracks for the current albumkey from mediastore
List<TrackModel> tracks = MusicLibraryHelper.getTracksForAlbum(albumKey, getApplicationContext());
// add tracks to current playlist
enqueueTracks(tracks);
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
public void enqueueRecentAlbums() {
// TODO check if this is working properly
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
List<TrackModel> tracks = MusicLibraryHelper.getRecentTracks(getApplicationContext());
enqueueTracks(tracks);
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* Enqueue all tracks of an artist identified by the artistId.
*
* @param artistId The id of the artist
* @param orderKey String to specify the order of the tracks
*/
public void enqueueArtist(long artistId, String orderKey) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
// get all tracks for the current artistId from mediastore
List<TrackModel> tracks = MusicLibraryHelper.getTracksForArtist(artistId, orderKey, getApplicationContext());
// add tracks to current playlist
enqueueTracks(tracks);
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* Enqueue the given track.
*
* @param track the current trackmodel
* @param asNext flag if the track should be enqueued as next
*/
public void enqueueTrack(TrackModel track, boolean asNext) {
if (asNext) {
enqueueAsNextTrack(track);
} else {
enqueueTrack(track);
}
}
/**
* Enqueue the given track.
*
* @param track the current trackmodel
*/
private void enqueueTrack(TrackModel track) {
// Check if current song is old last one, if so set next song to MP for
// gapless playback
int oldSize = mCurrentList.size();
mCurrentList.add(track);
/*
* If currently playing and playing is the last one in old playlist set
* enqueued one to next one for gapless mediaplayback
*/
if (mCurrentPlayingIndex == (oldSize - 1) && (oldSize != 0)) {
// Next song for MP has to be set for gapless mediaplayback
mNextPlayingIndex = mCurrentPlayingIndex + 1;
setNextTrackForMP();
}
// Send new NowPlaying because playlist changed
mPlaybackServiceStatusHelper.updateStatus();
}
/**
* Enqueue the given track as next.
*
* @param track the current trackmodel
*/
private void enqueueAsNextTrack(TrackModel track) {
// Check if currently playing index is set to a valid value
if (mCurrentPlayingIndex >= 0) {
// Enqueue in list structure
mCurrentList.add(mCurrentPlayingIndex + 1, track);
mNextPlayingIndex = mCurrentPlayingIndex + 1;
// Set next track to new one
setNextTrackForMP();
} else {
// If not playing just add it to the beginning of the playlist
mCurrentList.add(0, track);
// Start playback which is probably intended
jumpToIndex(0);
}
// Send new NowPlaying because playlist changed
mPlaybackServiceStatusHelper.updateStatus();
}
/**
* Dequeues a track from the playlist
*
* @param index Position of the track to remove
*/
public void dequeueTrack(int index) {
// Check if track is currently playing, if so stop it
if (mCurrentPlayingIndex == index) {
// Stop playback of currentsong
stop();
// Delete song at index
mCurrentList.remove(index);
// Jump to next song which should be at index now
// Jump is secured to check playlist bounds
jumpToIndex(index);
} else if ((mCurrentPlayingIndex + 1) == index) {
// Deletion of next song which requires extra handling
// because of gapless playback, set next song to next one
mCurrentList.remove(index);
setNextTrackForMP();
} else if (index >= 0 && index < mCurrentList.size()) {
mCurrentList.remove(index);
// mCurrentIndex and mNextPlayingIndex is now moved one position up so update variables
if (index < mCurrentPlayingIndex) {
mCurrentPlayingIndex--;
mNextPlayingIndex--;
}
}
// Send new NowPlaying because playlist changed
mPlaybackServiceStatusHelper.updateStatus();
}
/**
* Dequeues a section from a playlist
* A section is defined as a bunch of tracks from the same album.
*
* @param index start position of the section to remove
*/
public void dequeueTracks(int index) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
int endIndex = index + 1;
String albumKey = mCurrentList.get(index).getTrackAlbumKey();
// get endindex for section
while (endIndex < mCurrentList.size()) {
if (albumKey.equals(mCurrentList.get(endIndex).getTrackAlbumKey())) {
endIndex++;
} else {
break;
}
}
if (mCurrentPlayingIndex >= index && mCurrentPlayingIndex < endIndex) {
// current song is in section so stop playing
stop();
// remove section and update endindex accordingly
ListIterator<TrackModel> iterator = mCurrentList.listIterator(index);
while (iterator.hasNext()) {
TrackModel track = iterator.next();
if (albumKey.equals(track.getTrackAlbumKey())) {
iterator.remove();
endIndex--;
} else {
break;
}
}
// Jump to next song which should be at endIndex now
// Jump is secured to check playlist bounds
jumpToIndex(endIndex);
} else if ((mCurrentPlayingIndex + 1) == index) {
// Deletion of next song which requires extra handling
// because of gapless playback, set next song to next one
// remove section
ListIterator<TrackModel> iterator = mCurrentList.listIterator(index);
while (iterator.hasNext()) {
TrackModel track = iterator.next();
if (albumKey.equals(track.getTrackAlbumKey())) {
iterator.remove();
} else {
break;
}
}
setNextTrackForMP();
} else if (index >= 0 && index < mCurrentList.size()) {
// check if section is before current song
boolean beforeCurrentTrack = endIndex <= mCurrentPlayingIndex;
ListIterator<TrackModel> iterator = mCurrentList.listIterator(index);
while (iterator.hasNext()) {
TrackModel track = iterator.next();
if (albumKey.equals(track.getTrackAlbumKey())) {
iterator.remove();
if (beforeCurrentTrack) {
// if section is before current song update mCurrentPlayingIndex and mNextPlayingIndex
mCurrentPlayingIndex--;
mNextPlayingIndex--;
}
} else {
break;
}
}
}
// Send new NowPlaying because playlist changed
mPlaybackServiceStatusHelper.updateStatus();
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* Stops the gapless mediaplayer and cancels the foreground service. Removes
* any ongoing notification.
*/
public void stopService() {
// Cancel possible cancel timers
cancelQuitAlert();
// Save the current playback position
mLastPosition = getTrackPosition();
// If it is still running stop playback.
PLAYSTATE state = getPlaybackState();
if (state == PLAYSTATE.PLAYING || state == PLAYSTATE.PAUSE) {
mPlayer.stop();
}
// Save the state of the PBS at once
if (mCurrentList.size() > 0) {
OdysseyServiceState serviceState = new OdysseyServiceState();
serviceState.mTrackNumber = mCurrentPlayingIndex;
serviceState.mTrackPosition = mLastPosition;
serviceState.mRandomState = mRandom;
serviceState.mRepeatState = mRepeat;
mDatabaseManager.saveState(mCurrentList, serviceState, "auto", true);
}
// Final status update
mPlaybackServiceStatusHelper.updateStatus();
// Stops the service itself.
stopSelf();
}
/**
* @return The current random state of this service
*/
public RANDOMSTATE getRandom() {
return mRandom;
}
/**
* @return The current repeat state of this service
*/
public REPEATSTATE getRepeat() {
return mRepeat;
}
/**
* Enables/disables repeat function. If enabling check if end of playlist is
* already reached and then set next track to track0.
* <p/>
* If disabling check if last track plays.
*/
public void toggleRepeat() {
// get all repeat states
REPEATSTATE[] repeatstates = REPEATSTATE.values();
// toggle the repeat state
mRepeat = repeatstates[(mRepeat.ordinal() + 1) % repeatstates.length];
// update the status
mPlaybackServiceStatusHelper.updateStatus();
switch (mRepeat) {
case REPEAT_OFF:
// If playing last track, next track must be invalid
if (mCurrentPlayingIndex == mCurrentList.size() - 1) {
mNextPlayingIndex = -1;
} else {
mNextPlayingIndex = mCurrentPlayingIndex + 1;
}
setNextTrackForMP();
break;
case REPEAT_ALL:
// If playing last track, next must be first in playlist
if (mCurrentPlayingIndex == mCurrentList.size() - 1) {
mNextPlayingIndex = 0;
} else {
mNextPlayingIndex = mCurrentPlayingIndex + 1;
}
setNextTrackForMP();
break;
case REPEAT_TRACK:
// Next track must be same track again
mNextPlayingIndex = mCurrentPlayingIndex;
setNextTrackForMP();
break;
}
}
/**
* Enables/disables the random function. If enabling randomize next song and
* notify the gaplessPlayer about the new track. If deactivating set check
* if new track exists and set it to this.
*/
public void toggleRandom() {
// get all random states
RANDOMSTATE[] randomstates = RANDOMSTATE.values();
// toggle the random state
mRandom = randomstates[(mRandom.ordinal() + 1) % randomstates.length];
// update the status
mPlaybackServiceStatusHelper.updateStatus();
if (mRandom == RANDOMSTATE.RANDOM_ON) {
randomizeNextTrack();
} else {
// Set nextTrack to next in list
if ((mCurrentPlayingIndex + 1 < mCurrentList.size()) && mCurrentPlayingIndex >= 0) {
mNextPlayingIndex = mCurrentPlayingIndex + 1;
}
}
// Notify GaplessPlayer
setNextTrackForMP();
}
/**
* Returns the index of the currently playing/paused track
*/
public int getCurrentIndex() {
return mCurrentPlayingIndex;
}
/**
* Returns current track if any is playing/paused at the moment.
*/
public TrackModel getCurrentTrack() {
if (mCurrentPlayingIndex >= 0 && mCurrentList.size() > mCurrentPlayingIndex) {
return mCurrentList.get(mCurrentPlayingIndex);
}
return null;
}
/**
* Return the current nowplaying information including the current track.
*/
public NowPlayingInformation getNowPlayingInformation() {
PLAYSTATE state = getPlaybackState();
if (state == PLAYSTATE.STOPPED) {
return new NowPlayingInformation();
} else {
TrackModel currentTrack = mCurrentList.get(mCurrentPlayingIndex);
return new NowPlayingInformation(state, mCurrentPlayingIndex, mRepeat, mRandom, mCurrentList.size(), currentTrack);
}
}
/**
* Save the current playlist in mediastore
*/
public void savePlaylist(String name) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
MusicLibraryHelper.savePlaylist(name, mCurrentList, getApplicationContext());
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* enqueue a selected playlist from mediastore
*
* @param playlistId the id of the selected playlist
*/
public void enqueuePlaylist(long playlistId) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
// get playlist from mediastore
List<TrackModel> playlistTracks = MusicLibraryHelper.getTracksForPlaylist(playlistId, getApplicationContext());
// add tracks to current playlist
enqueueTracks(playlistTracks);
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* Resume the bookmark with the given timestamp
*/
public void resumeBookmark(long timestamp) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
// clear current playlist
clearPlaylist();
// get playlist from database
mCurrentList = mDatabaseManager.readPlaylist(timestamp);
// get state from database
OdysseyServiceState state = mDatabaseManager.getState(timestamp);
// Copy the loaded state to internal state
mCurrentPlayingIndex = state.mTrackNumber;
mLastPosition = state.mTrackPosition;
mRandom = state.mRandomState;
mRepeat = state.mRepeatState;
// Check if playlist bounds match loaded indices
if (mCurrentPlayingIndex < 0 || mCurrentPlayingIndex > mCurrentList.size()) {
mCurrentPlayingIndex = -1;
}
mLastPlayingIndex = -1;
mNextPlayingIndex = -1;
// call resume and start playback
resume();
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* Delete the bookmark with the given timestamp from the database.
*/
public void deleteBookmark(long timestamp) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
// delete wont affect current playback
// so just delete the state and the playlist from the database
mDatabaseManager.removeState(timestamp);
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* Create a bookmark with the given title and save it in the database.
*/
public void createBookmark(String bookmarkTitle) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
// grab the current state and playlist and save this as a new bookmark with the given title
OdysseyServiceState serviceState = new OdysseyServiceState();
// Move internal state to the new created state object
serviceState.mTrackNumber = mCurrentPlayingIndex;
serviceState.mTrackPosition = getTrackPosition();
serviceState.mRandomState = mRandom;
serviceState.mRepeatState = mRepeat;
mDatabaseManager.saveState(mCurrentList, serviceState, bookmarkTitle, false);
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* creates a trackmodel for a given filepath and add the track to the playlist
*
* @param filePath the path to the selected file
* @param asNext flag if the file should be enqueued as next
*/
public void enqueueFile(String filePath, boolean asNext) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
FileModel currentFile = new FileModel(filePath);
TrackModel track = FileExplorerHelper.getInstance().getTrackModelForFile(getApplicationContext(), currentFile);
enqueueTrack(track, asNext);
// Send new NowPlaying because playlist changed
mPlaybackServiceStatusHelper.updateStatus();
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* creates trackmodels for a given directorypath and adds the tracks to the playlist
*
* @param directoryPath the path to the selected directory
*/
public void enqueueDirectory(String directoryPath) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
FileModel currentDirectory = new FileModel(directoryPath);
List<TrackModel> tracks = FileExplorerHelper.getInstance().getTrackModelsForFolder(getApplicationContext(), currentDirectory);
// add tracks to current playlist
enqueueTracks(tracks);
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* creates trackmodels for a given directorypath (inclusive all subdirectories) and adds the tracks to the playlist
*
* @param directoryPath the path to the selected directory
*/
public void enqueueDirectoryAndSubDirectories(String directoryPath) {
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.WORKING);
mBusy = true;
FileModel currentDirectory = new FileModel(directoryPath);
List<TrackModel> tracks = FileExplorerHelper.getInstance().getTrackModelsForFolderAndSubFolders(getApplicationContext(), currentDirectory);
// add tracks to current playlist
enqueueTracks(tracks);
mPlaybackServiceStatusHelper.broadcastPlaybackServiceState(PLAYBACKSERVICESTATE.IDLE);
mBusy = false;
}
/**
* Returns the playback state of the service
*/
public PLAYSTATE getPlaybackState() {
if (mCurrentList.size() > 0 && mCurrentPlayingIndex >= 0) {
// Check current playback state. If playing inform all listeners and
// check if notification is set, and set if not.
if (mPlayer.isRunning() && (mCurrentPlayingIndex < mCurrentList.size())) {
// Player is running and current index seems to be valid
return PLAYSTATE.PLAYING;
} else if (!mPlayer.isPrepared()) {
// If a position is set but the player is not prepared yet it is clear, that the user has not played the song yet
return PLAYSTATE.RESUMED;
} else {
// Only case left is that the player is paused, because a track is set AND PREPARED so it was played already
return PLAYSTATE.PAUSE;
}
} else {
// No playback because list is empty
return PLAYSTATE.STOPPED;
}
}
/**
* Returns the working state of the service
*
* @return true if the service is busy else false
*/
public boolean isBusy() {
return mBusy;
}
public void hideArtwork(boolean enable) {
mPlaybackServiceStatusHelper.hideArtwork(enable);
}
/**
* Handles all the exceptions from the GaplessPlayer. For now it justs stops
* itself and outs an Toast message to the user. Thats the best we could
* think of now :P.
*/
private void handlePlaybackException(GaplessPlayer.PlaybackException exception) {
Log.v(TAG, "Exception occured: " + exception.getReason().toString());
Toast.makeText(getBaseContext(), TAG + ":" + exception.getReason().toString(), Toast.LENGTH_LONG).show();
// TODO better handling?
// Stop service on exception for now
stopService();
}
/**
* Sets the index, of the track to play next,to a random generated one.
*/
private void randomizeNextTrack() {
// Set next index to random one
if (mCurrentList.size() > 0) {
mNextPlayingIndex = mRandomGenerator.nextInt(mCurrentList.size());
// if next index equal to current index create a new random
// index but just trying RANDOM_RETRIES times
int counter = 0;
while (mNextPlayingIndex == mCurrentPlayingIndex && counter < RANDOM_RETRIES) {
mNextPlayingIndex = mRandomGenerator.nextInt(mCurrentList.size());
counter++;
}
}
}
/**
* Sets the next track of the GaplessPlayer to the nextTrack in the queue so
* there can be a smooth transition from one track to the next one.
*/
private void setNextTrackForMP() {
// If player is not running or at least prepared, this makes no sense
if (mPlayer.isPrepared() || mPlayer.isRunning()) {
// Sets the next track for gapless playing
if (mNextPlayingIndex >= 0 && mNextPlayingIndex < mCurrentList.size()) {
try {
mPlayer.setNextTrack(mCurrentList.get(mNextPlayingIndex).getTrackURL());
} catch (GaplessPlayer.PlaybackException e) {
handlePlaybackException(e);
}
} else {
try {
/*
* No tracks remains. So set it to null. GaplessPlayer knows
* how to handle this :)
*/
mPlayer.setNextTrack(null);
} catch (GaplessPlayer.PlaybackException e) {
handlePlaybackException(e);
}
}
}
}
/**
* Listener class for playback begin of the GaplessPlayer. Handles the
* different scenarios: If no random playback is active, check if new track
* is ready and set index and GaplessPlayer to it. If no track remains in
* queue check if repeat is activated and if reset queue to track 0. If
* not generate a random index and set GaplessPlayer to that random track.
*/
private class PlaybackStartListener implements GaplessPlayer.OnTrackStartedListener {
@Override
public void onTrackStarted(String URI) {
// Move the index to the next one
mCurrentPlayingIndex = mNextPlayingIndex;
if (mCurrentPlayingIndex >= 0 && mCurrentPlayingIndex < mCurrentList.size()) {
// Broadcast simple.last.fm.scrobble broadcast about the started track
TrackModel newTrackModel = mCurrentList.get(mCurrentPlayingIndex);
mPlaybackServiceStatusHelper.notifyLastFM(newTrackModel, PlaybackServiceStatusHelper.SLS_STATES.SLS_START);
}
// Notify all the things
mPlaybackServiceStatusHelper.updateStatus();
if (mRandom == RANDOMSTATE.RANDOM_OFF) {
// Check the repeat state
switch (mRepeat) {
case REPEAT_OFF:
// Repeat off so next track is the next track in the playlist if available
if (mCurrentPlayingIndex + 1 < mCurrentList.size()) {
mNextPlayingIndex = mCurrentPlayingIndex + 1;
} else {
mNextPlayingIndex = -1;
}
break;
case REPEAT_ALL:
// Repeat playlist so set to first PL song if last song is
// reached
if (mCurrentList.size() > 0 && mCurrentPlayingIndex + 1 == mCurrentList.size()) {
mNextPlayingIndex = 0;
} else if (mCurrentList.size() > 0 && mCurrentPlayingIndex + 1 < mCurrentList.size()) {
// If the end of the playlist was not reached move to the next track
mNextPlayingIndex = mCurrentPlayingIndex + 1;
}
break;
case REPEAT_TRACK:
// Repeat track so next track is the current track
mNextPlayingIndex = mCurrentPlayingIndex;
break;
}
} else {
// Random on
randomizeNextTrack();
}
// Sets the next track for gapless playing
setNextTrackForMP();
// Check if temporary wakelock is held to prevent device from shutting down during
// the short transition
if (mSongTransitionWakelock.isHeld()) {
// we could release wakelock here again, because the GaplessPlayer is now wakelocking again
mSongTransitionWakelock.release();
}
}
}
/**
* Listener class for the GaplessPlayer. If a track finishes playback send
* it to the simple last.fm scrobbler. Check if this was the last track of
* the queue and if so send an update to all the things like
* notification,broadcastreceivers and lockscreen. Stop the service.
*/
private class PlaybackFinishListener implements GaplessPlayer.OnTrackFinishedListener {
@Override
public void onTrackFinished() {
// Remember the last track index for moving backwards in the queue.
mLastPlayingIndex = mCurrentPlayingIndex;
if (mCurrentList.size() > 0 && mCurrentPlayingIndex >= 0 && (mCurrentPlayingIndex < mCurrentList.size())) {
// Broadcast simple.last.fm.scrobble broadcast about the track finish
TrackModel item = mCurrentList.get(mCurrentPlayingIndex);
mPlaybackServiceStatusHelper.notifyLastFM(item, PlaybackServiceStatusHelper.SLS_STATES.SLS_COMPLETE);
}
// No more tracks
if (mNextPlayingIndex == -1) {
stop();
}
}
}
/**
* Callback method for AudioFocus changes. If audio focus change a call got
* in or a notification for example. React to the different kinds of
* changes. Resumes on regaining.
*/
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
// If we are ducked at the moment, return volume to full output
if (mIsDucked) {
mPlayer.setVolume(1.0f, 1.0f);
mIsDucked = false;
} else if (mLostAudioFocus) {
// If we temporarily lost the audio focus we can resume playback here
resume();
mLostAudioFocus = false;
}
break;
case AudioManager.AUDIOFOCUS_LOSS:
// Pause playback here, because we lost audio focus (not temporarily)
if (mPlayer.isRunning()) {
pause();
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// Pause audio for the moment of focus loss, will be resumed.
if (mPlayer.isRunning()) {
pause();
mLostAudioFocus = true;
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// We only need to duck our volume, so set it to 10%? and save that we ducked for resuming
if (mPlayer.isRunning()) {
mPlayer.setVolume(0.1f, 0.1f);
mIsDucked = true;
}
break;
default:
break;
}
}
/**
* Receiver class for all the different broadcasts which are able to control
* the PlaybackService. Also the receiver for the noisy event (e.x.
* headphone unplugging)
*/
private class BroadcastControlReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
/* Check if audio focus is currently lost. For example an incoming call gets picked up
and now the user disconnects the headphone. The music should not resume when the call
is finished and the audio focus is regained.
*/
if (mLostAudioFocus) {
mLostAudioFocus = false;
}
pause();
} else if (intent.getAction().equals(ACTION_PLAY)) {
resume();
} else if (intent.getAction().equals(ACTION_PAUSE)) {
pause();
} else if (intent.getAction().equals(ACTION_NEXT)) {
setNextTrack();
} else if (intent.getAction().equals(ACTION_PREVIOUS)) {
setPreviousTrack();
} else if (intent.getAction().equals(ACTION_STOP)) {
stop();
} else if (intent.getAction().equals(ACTION_TOGGLEPAUSE)) {
togglePause();
} else if (intent.getAction().equals(ACTION_QUIT)) {
stopService();
} else if (intent.getAction().equals(ArtworkManager.ACTION_NEW_ARTWORK_READY)) {
// Check if artwork is for currently playing album
String albumKey = intent.getStringExtra(ArtworkManager.INTENT_EXTRA_KEY_ALBUM_KEY);
mPlaybackServiceStatusHelper.newAlbumArtworkReady(albumKey);
}
}
}
}