package com.kure.musicplayer.services; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Random; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.RemoteControlClient; import android.net.Uri; import android.os.Binder; import android.os.IBinder; import android.os.PowerManager; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; import com.kure.musicplayer.NotificationMusic; import com.kure.musicplayer.R; import com.kure.musicplayer.kMP; import com.kure.musicplayer.external.RemoteControlClientCompat; import com.kure.musicplayer.external.RemoteControlHelper; import com.kure.musicplayer.model.Song; /** * Service that makes the music play and notifies every action. * * Tasks: * * - Abstracts controlling the native Android MediaPlayer; * - Keep showing a system Notification with info on * currently playing song; * - Starts the other service, `MusicScrobblerService` * (if set on Settings) that scrobbles songs to Last.fm; * - LocalBroadcasts every action it takes; * - Keep watching for headphone/headset events with * a Broadcast - and react accordingly. * * Broadcasts: * * This service makes sure to broadcast every action it * takes. * * It sends a LocalBroadcast of name `BROADCAST_EVENT_NAME`, * of which you can get it's action with the following * extras: * * - String BROADCAST_EXTRA_ACTION: Current action it's taking. * * - Long BROADCAST_EXTRA_SONG_ID: ID of the Song it's taking * action into. * * For example, see the following scenarios: * * - Starts playing Song with ID 1. * + Send a LocalBroadcast with `BROADCAST_EXTRA_ACTION` * of `BROADCAST_EXTRA_PLAYING` and * `BROADCAST_EXTRA_SONG_ID` of 1. * * - User skips to a Song with ID 2: * + Send a LocalBroadcast with `BROADCAST_EXTRA_ACTION` * of `BROADCAST_EXTRA_SKIP_NEXT` and * `BROADCAST_EXTRA_SONG_ID` of 1. * + Send a LocalBriadcast with `BROADCAST_EXTRA_ACTION` * of `BROADCAST_EXTRA_PLAYING` and * `BROADCAST_EXTRA_SONG_ID` of 2. * * @note It keeps the music playing even when the * device is locked. * For that, we must add a special permission * on the AndroidManifest. * * Thanks: * - Google's MediaPlayer guide - has info on AudioFocus, * Services and lotsa stuff * http://developer.android.com/guide/topics/media/mediaplayer.html */ public class ServicePlayMusic extends Service implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener, AudioManager.OnAudioFocusChangeListener { /** * String that identifies all broadcasts this Service makes. * * Since this Service will send LocalBroadcasts to explain * what it does (like "playing song" or "paused song"), * other classes that might be interested on it must * register a BroadcastReceiver to this String. */ public static final String BROADCAST_ACTION = "com.kure.musicplayer.MUSIC_SERVICE"; /** String used to get the current state Extra on the Broadcast Intent */ public static final String BROADCAST_EXTRA_STATE = "x_japan"; /** String used to get the song ID Extra on the Broadcast Intent */ public static final String BROADCAST_EXTRA_SONG_ID = "tenacious_d"; // All possible messages this Service will broadcast // Ignore the actual values /** Broadcast for when some music started playing */ public static final String BROADCAST_EXTRA_PLAYING = "beatles"; /** Broadcast for when some music just got paused */ public static final String BROADCAST_EXTRA_PAUSED = "santana"; /** Broadcast for when a paused music got unpaused*/ public static final String BROADCAST_EXTRA_UNPAUSED = "iron_maiden"; /** Broadcast for when current music got played until the end */ public static final String BROADCAST_EXTRA_COMPLETED = "los_hermanos"; /** Broadcast for when the user skipped to the next song */ public static final String BROADCAST_EXTRA_SKIP_NEXT = "paul_gilbert"; /** Broadcast for when the user skipped to the previous song */ public static final String BROADCAST_EXTRA_SKIP_PREVIOUS = "john_petrucci"; /** * Android Media Player - we control it in here. */ private MediaPlayer player; /** * List of songs we're currently playing. */ private ArrayList<Song> songs; /** * Index of the current song we're playing on the `songs` list. */ public int currentSongPosition; /** * Copy of the current song being played (or paused). * * Use it to get info from the current song. */ public Song currentSong = null; /** * Flag that indicates whether we're at Shuffle mode. */ private boolean shuffleMode = false; /** * Random number generator for the Shuffle Mode. */ private Random randomNumberGenerator; private boolean repeatMode = false; /** * Spawns an on-going notification with our current * playing song. */ private NotificationMusic notification = null; // The tag we put on debug messages final static String TAG = "MusicService"; // These are the Intent actions that we are prepared to handle. Notice that the fact these // constants exist in our class is a mere convenience: what really defines the actions our // service can handle are the <action> tags in the <intent-filters> tag for our service in // AndroidManifest.xml. public static final String BROADCAST_ORDER = "com.kure.musicplayer.MUSIC_SERVICE"; public static final String BROADCAST_EXTRA_GET_ORDER = "com.kure.musicplayer.dasdas.MUSIC_SERVICE"; public static final String BROADCAST_ORDER_PLAY = "com.kure.musicplayer.action.PLAY"; public static final String BROADCAST_ORDER_PAUSE = "com.kure.musicplayer.action.PAUSE"; public static final String BROADCAST_ORDER_TOGGLE_PLAYBACK = "dlsadasd"; public static final String BROADCAST_ORDER_STOP = "com.kure.musicplayer.action.STOP"; public static final String BROADCAST_ORDER_SKIP = "com.kure.musicplayer.action.SKIP"; public static final String BROADCAST_ORDER_REWIND = "com.kure.musicplayer.action.REWIND"; /** * Possible states this Service can be on. */ enum ServiceState { // MediaPlayer is stopped and not prepared to play Stopped, // MediaPlayer is preparing... Preparing, // Playback active - media player ready! // (but the media player may actually be paused in // this state if we don't have audio focus). Playing, // So that we know we have to resume playback once we get focus back) // playback paused (media player ready!) Paused }; /** * Current state of the Service. */ ServiceState serviceState = ServiceState.Preparing; /** * Controller that communicates with the lock screen, * providing that fancy widget. */ RemoteControlClientCompat lockscreenController = null; /** * We use this to get the media buttons' Broadcasts and * to control the lock screen widget. * * Component name of the MusicIntentReceiver. */ ComponentName mediaButtonEventReceiver; /** * Use this to get audio focus: * * 1. Making sure other music apps don't play * at the same time; * 2. Guaranteeing the lock screen widget will * be controlled by us; */ AudioManager audioManager; /** * Whenever we're created, reset the MusicPlayer and * start the MusicScrobblerService. */ public void onCreate() { super.onCreate(); currentSongPosition = 0; randomNumberGenerator = new Random(); audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); initMusicPlayer(); Context context = getApplicationContext(); // Starting the scrobbler service. //Intent scrobblerIntent = new Intent(context, ServiceScrobbleMusic.class); //context.startService(scrobblerIntent); // Registering our BroadcastReceiver to listen to orders // from inside our own application. LocalBroadcastManager .getInstance(getApplicationContext()) .registerReceiver(localBroadcastReceiver, new IntentFilter(ServicePlayMusic.BROADCAST_ORDER)); // Registering the headset broadcaster for info related // to user plugging the headset. IntentFilter headsetFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); registerReceiver(headsetBroadcastReceiver, headsetFilter); Log.w(TAG, "onCreate"); } /** * Initializes the Android's internal MediaPlayer. * * @note We might call this function several times without * necessarily calling {@link #stopMusicPlayer()}. */ public void initMusicPlayer() { if (player == null) player = new MediaPlayer(); // Assures the CPU continues running this service // even when the device is sleeping. player.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); player.setAudioStreamType(AudioManager.STREAM_MUSIC); // These are the events that will "wake us up" player.setOnPreparedListener(this); // player initialized player.setOnCompletionListener(this); // song completed player.setOnErrorListener(this); Log.w(TAG, "initMusicPlayer"); } /** * Cleans resources from Android's native MediaPlayer. * * @note According to the MediaPlayer guide, you should release * the MediaPlayer as often as possible. * For example, when losing Audio Focus for an extended * period of time. */ public void stopMusicPlayer() { if (player == null) return; player.stop(); player.release(); player = null; Log.w(TAG, "stopMusicPlayer"); } /** * Sets the "Now Playing List" * * @param theSongs Songs list that will play from now on. * * @note Make sure to call {@link #playSong()} after this. */ public void setList(ArrayList<Song> theSongs) { songs = theSongs; } /** * Appends a song to the end of the currently playing queue. * * @param song New song to put at the end. */ public void add(Song song) { songs.add(song); } /** * Receives external Broadcasts and gives our MusicService * orders based on them. * * It is the bridge between our application and the external * world. It receives Broadcasts and launches Internal Broadcasts. * * It acts on music events (such as disconnecting headphone) * and music controls (the lockscreen widget). * * @note This class works because we are declaring it in a * `receiver` tag in `AndroidManifest.xml`. * * @note It is static so we can look out for external broadcasts * even when the service is offline. */ public static class ExternalBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.w(TAG, "external broadcast"); // Broadcasting orders to our MusicService // locally (inside the application) LocalBroadcastManager local = LocalBroadcastManager.getInstance(context); String action = intent.getAction(); // Headphones disconnected if (action.equals(android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { // Will only pause the music if the Setting // for it is enabled. if (! kMP.settings.get("pause_headphone_off", true)) return; // ADD SETTINGS HERE String text = context.getString(R.string.service_music_play_headphone_off); Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); // send an intent to our MusicService to telling it to pause the audio Intent broadcastIntent = new Intent(ServicePlayMusic.BROADCAST_ORDER); broadcastIntent.putExtra(ServicePlayMusic.BROADCAST_EXTRA_GET_ORDER, ServicePlayMusic.BROADCAST_ORDER_PAUSE); local.sendBroadcast(broadcastIntent); Log.w(TAG, "becoming noisy"); return; } if (action.equals(Intent.ACTION_MEDIA_BUTTON)) { // Which media key was pressed KeyEvent keyEvent = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); // Not interested on anything other than pressed keys. if (keyEvent.getAction() != KeyEvent.ACTION_DOWN) //Action_down = A pressed gesture has started, the motion contains the initial starting location. return; String intentValue = null; switch (keyEvent.getKeyCode()) { case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: intentValue = ServicePlayMusic.BROADCAST_ORDER_TOGGLE_PLAYBACK; Log.w(TAG, "media play pause"); break; case KeyEvent.KEYCODE_MEDIA_PLAY: intentValue = ServicePlayMusic.BROADCAST_ORDER_PLAY; Log.w(TAG, "media play"); break; case KeyEvent.KEYCODE_MEDIA_PAUSE: intentValue = ServicePlayMusic.BROADCAST_ORDER_PAUSE; Log.w(TAG, "media pause"); break; case KeyEvent.KEYCODE_MEDIA_NEXT: intentValue = ServicePlayMusic.BROADCAST_ORDER_SKIP; Log.w(TAG, "media next"); break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: // TODO: ensure that doing this in rapid succession actually plays the // previous song intentValue = ServicePlayMusic.BROADCAST_ORDER_REWIND; Log.w(TAG, "media previous"); break; } // Actually sending the Intent if (intentValue != null) { Intent broadcastIntent = new Intent(ServicePlayMusic.BROADCAST_ORDER); broadcastIntent.putExtra(ServicePlayMusic.BROADCAST_EXTRA_GET_ORDER, intentValue); local.sendBroadcast(broadcastIntent); } } } }; /** * Will keep an eye on global broadcasts related to * the Headset. */ BroadcastReceiver headsetBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // Headphones just connected (or not) if (action.equals(Intent.ACTION_HEADSET_PLUG)) { Log.w(TAG, "headset plug"); boolean connectedHeadphones = (intent.getIntExtra("state", 0) == 1); boolean connectedMicrophone = (intent.getIntExtra("microphone", 0) == 1) && connectedHeadphones; // User just connected headphone and the player was paused, // so we shoud restart the music. if (connectedMicrophone && (serviceState == ServiceState.Paused)) { // Will only do it if it's Setting is enabled, of course if (kMP.settings.get("play_headphone_on", true)) { LocalBroadcastManager local = LocalBroadcastManager.getInstance(context); Intent broadcastIntent = new Intent(ServicePlayMusic.BROADCAST_ORDER); broadcastIntent.putExtra(ServicePlayMusic.BROADCAST_EXTRA_GET_ORDER, ServicePlayMusic.BROADCAST_ORDER_PLAY); local.sendBroadcast(broadcastIntent); } } // I wonder what's this for String headsetName = intent.getStringExtra("name"); if (connectedHeadphones) { String text = context.getString(R.string.service_music_play_headphone_on, headsetName); Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); } return; } } }; /** * The thing that will keep an eye on LocalBroadcasts * for the MusicService. */ BroadcastReceiver localBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // Getting the information sent by the MusicService // (and ignoring it if invalid) String order = intent.getStringExtra(ServicePlayMusic.BROADCAST_EXTRA_GET_ORDER); // return if we do not have any order if (order == null) return; if (order.equals(ServicePlayMusic.BROADCAST_ORDER_PAUSE)) { pausePlayer(); } else if (order.equals(ServicePlayMusic.BROADCAST_ORDER_PLAY)) { unpausePlayer(); } else if (order.equals(ServicePlayMusic.BROADCAST_ORDER_TOGGLE_PLAYBACK)) { togglePlayback(); } else if (order.equals(ServicePlayMusic.BROADCAST_ORDER_SKIP)) { next(true); playSong(); } else if (order.equals(ServicePlayMusic.BROADCAST_ORDER_REWIND)) { previous(true); playSong(); } Log.w(TAG, "local broadcast received"); } }; /** * Asks the AudioManager for our application to * have the audio focus. * * @return If we have it. */ private boolean requestAudioFocus() { //Request audio focus for playback int result = audioManager.requestAudioFocus( this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); //Check if audio focus was granted. If not, stop the service. return (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED); } /** * Does something when the audio focus state changed * * @note Meaning it runs when we get and when we don't get * the audio focus from `#requestAudioFocus()`. * * For example, when we receive a message, we lose the focus * and when the ringer stops playing, we get the focus again. * * So we must avoid the bug that occurs when the user pauses * the player but receives a message - and since after that * we get the focus, the player will unpause. */ public void onAudioFocusChange(int focusChange) { switch (focusChange) { // Yay, gained audio focus! Either from losing it for // a long or short periods of time. case AudioManager.AUDIOFOCUS_GAIN: Log.w(TAG, "audiofocus gain"); if (player == null) initMusicPlayer(); if (pausedTemporarilyDueToAudioFocus) { pausedTemporarilyDueToAudioFocus = false; unpausePlayer(); } if (loweredVolumeDueToAudioFocus) { loweredVolumeDueToAudioFocus = false; player.setVolume(1.0f, 1.0f); } break; // Damn, lost the audio focus for a (presumable) long time case AudioManager.AUDIOFOCUS_LOSS: Log.w(TAG, "audiofocus loss"); // Giving up everything //audioManager.unregisterMediaButtonEventReceiver(mediaButtonEventReceiver); //audioManager.abandonAudioFocus(this); //pausePlayer(); stopMusicPlayer(); break; // Just lost audio focus but will get it back shortly case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: Log.w(TAG, "audiofocus loss transient"); if (! isPaused()) { pausePlayer(); pausedTemporarilyDueToAudioFocus = true; } break; // Temporarily lost audio focus but I can keep it playing // at a low volume instead of stopping completely case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: Log.w(TAG, "audiofocus loss transient can duck"); player.setVolume(0.1f, 0.1f); loweredVolumeDueToAudioFocus = true; break; } } // Internal flags for the function above {{ private boolean pausedTemporarilyDueToAudioFocus = false; private boolean loweredVolumeDueToAudioFocus = false; // }} /** * Updates the lock-screen widget (creating if non-existing). * * @param song Where it will take metadata to display. * * @param state Which state is it into. * Can be one of the following: * {@link RemoteControlClient.PLAYSTATE_PLAYING } * {@link RemoteControlClient.PLAYSTATE_PAUSED } * {@link RemoteControlClient.PLAYSTATE_BUFFERING } * {@link RemoteControlClient.PLAYSTATE_ERROR } * {@link RemoteControlClient.PLAYSTATE_FAST_FORWARDING } * {@link RemoteControlClient.PLAYSTATE_REWINDING } * {@link RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS } * {@link RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS } * {@link RemoteControlClient.PLAYSTATE_STOPPED } */ public void updateLockScreenWidget(Song song, int state) { // Only showing if the Setting is... well... set if (! kMP.settings.get("show_lock_widget", true)) return; if (song == null) return; if (!requestAudioFocus()) { //Stop the service. stopSelf(); Toast.makeText(getApplicationContext(), "Audio Focus is not granted", Toast.LENGTH_LONG).show(); return; } Log.w("service", "audio_focus_granted"); // The Lock-Screen widget was not created up until now. // (both of the null-checks below) if (mediaButtonEventReceiver == null) mediaButtonEventReceiver = new ComponentName(this, ExternalBroadcastReceiver.class); if (lockscreenController == null) { Intent audioButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); audioButtonIntent.setComponent(mediaButtonEventReceiver); PendingIntent pending = PendingIntent.getBroadcast(this, 0, audioButtonIntent, 0); lockscreenController = new RemoteControlClientCompat(pending); RemoteControlHelper.registerRemoteControlClient(audioManager, lockscreenController); audioManager.registerMediaButtonEventReceiver(mediaButtonEventReceiver); Log.w("service", "created control compat"); } // Current state of the Lock-Screen Widget lockscreenController.setPlaybackState(state); // All buttons the Lock-Screen Widget supports // (will be broadcasts) lockscreenController.setTransportControlFlags( RemoteControlClient.FLAG_KEY_MEDIA_PLAY | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS | RemoteControlClient.FLAG_KEY_MEDIA_NEXT); // Update the current song metadata // on the Lock-Screen Widget lockscreenController // Starts editing (before #apply()) .editMetadata(true) // Sending all metadata of the current song .putString(android.media.MediaMetadataRetriever.METADATA_KEY_ARTIST, song.getArtist()) .putString(android.media.MediaMetadataRetriever.METADATA_KEY_ALBUM, song.getAlbum()) .putString(android.media.MediaMetadataRetriever.METADATA_KEY_TITLE, song.getTitle()) .putLong (android.media.MediaMetadataRetriever.METADATA_KEY_DURATION, song.getDuration()) // TODO: fetch real item artwork //.putBitmap( // RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK, // mDummyAlbumArt) // Saves (after #editMetadata()) .apply(); Log.w("service", "remote control client applied"); } public void destroyLockScreenWidget() { if ((audioManager != null) && (lockscreenController != null)) { //RemoteControlHelper.unregisterRemoteControlClient(audioManager, lockscreenController); lockscreenController = null; } if ((audioManager != null) && (mediaButtonEventReceiver != null)) { audioManager.unregisterMediaButtonEventReceiver(mediaButtonEventReceiver); mediaButtonEventReceiver = null; } } /** * Called when the music is ready for playback. */ @Override public void onPrepared(MediaPlayer mp) { serviceState = ServiceState.Playing; // Start playback player.start(); // If the user clicks on the notification, let's spawn the // Now Playing screen. notifyCurrentSong(); } /** * Sets a specific song, already within internal Now Playing List. * * @param songIndex Index of the song inside the Now Playing List. */ public void setSong(int songIndex) { if (songIndex < 0 || songIndex >= songs.size()) currentSongPosition = 0; else currentSongPosition = songIndex; } /** * Will be called when the music completes - either when the * user presses 'next' or when the music ends or when the user * selects another track. */ @Override public void onCompletion(MediaPlayer mp) { // Keep this state! serviceState = ServiceState.Playing; /* if (player.getCurrentPosition() <= 0) return; */ broadcastState(ServicePlayMusic.BROADCAST_EXTRA_COMPLETED); // Repeating current song if desired if (repeatMode) { playSong(); return; } // Remember that by calling next(), if played // the last song on the list, will reset to the // first one. next(false); // Reached the end, should we restart playing // from the first song or simply stop? if (currentSongPosition == 0) { if (kMP.settings.get("repeat_list", false)) playSong(); else destroySelf(); return; } // Common case - skipped a track or anything playSong(); } /** * If something wrong happens with the MusicPlayer. */ @Override public boolean onError(MediaPlayer mp, int what, int extra) { mp.reset(); Log.w(TAG, "onError"); return false; } @Override public void onDestroy() { Context context = getApplicationContext(); cancelNotification(); currentSong = null; // Stopping the scrobbler service. //Intent scrobblerIntent = new Intent(context, ServiceScrobbleMusic.class); //context.stopService(scrobblerIntent); if (audioManager != null) audioManager.abandonAudioFocus(this); stopMusicPlayer(); destroyLockScreenWidget(); Log.w(TAG, "onDestroy"); super.onDestroy(); } /** * Kills the service. * * @note Explicitly call this when the service is completed * or whatnot. */ private void destroySelf() { stopSelf(); currentSong = null; } // These methods are to be called by the Activity // to work on the music-playing. /** * Jumps to the previous song on the list. * * @note Remember to call `playSong()` to make the MusicPlayer * actually play the music. */ public void previous(boolean userSkippedSong) { if (serviceState != ServiceState.Paused && serviceState != ServiceState.Playing) return; if (userSkippedSong) broadcastState(ServicePlayMusic.BROADCAST_EXTRA_SKIP_PREVIOUS); // Updates Lock-Screen Widget if (lockscreenController != null) lockscreenController.setPlaybackState(RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS); currentSongPosition--; if (currentSongPosition < 0) currentSongPosition = songs.size() - 1; } /** * Jumps to the next song on the list. * * @note Remember to call `playSong()` to make the MusicPlayer * actually play the music. */ public void next(boolean userSkippedSong) { if (serviceState != ServiceState.Paused && serviceState != ServiceState.Playing) return; // TODO implement a queue of songs to prevent last songs // to be played // TODO or maybe a playlist, whatever if (userSkippedSong) broadcastState(ServicePlayMusic.BROADCAST_EXTRA_SKIP_NEXT); // Updates Lock-Screen Widget if (lockscreenController != null) lockscreenController.setPlaybackState(RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS); if (shuffleMode) { int newSongPosition = currentSongPosition; while (newSongPosition == currentSongPosition) newSongPosition = randomNumberGenerator.nextInt(songs.size()); currentSongPosition = newSongPosition; return; } currentSongPosition++; if (currentSongPosition >= songs.size()) currentSongPosition = 0; } public int getPosition() { return player.getCurrentPosition(); } public int getDuration() { return player.getDuration(); } public boolean isPlaying() { boolean returnValue = false; try { returnValue = player.isPlaying(); } catch (IllegalStateException e) { player.reset(); player.prepareAsync(); } return returnValue; } public boolean isPaused() { return serviceState == ServiceState.Paused; } /** * Actually plays the song set by `currentSongPosition`. */ public void playSong() { player.reset(); // Get the song ID from the list, extract the ID and // get an URL based on it Song songToPlay = songs.get(currentSongPosition); currentSong = songToPlay; // Append the external URI with our songs' Uri songToPlayURI = ContentUris.withAppendedId (android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, songToPlay.getId()); try { player.setDataSource(getApplicationContext(), songToPlayURI); } catch(IOException io) { Log.e(TAG, "IOException: couldn't change the song", io); destroySelf(); } catch(Exception e) { Log.e(TAG, "Error when changing the song", e); destroySelf(); } // Prepare the MusicPlayer asynchronously. // When finished, will call `onPrepare` player.prepareAsync(); serviceState = ServiceState.Preparing; broadcastState(ServicePlayMusic.BROADCAST_EXTRA_PLAYING); updateLockScreenWidget(currentSong, RemoteControlClient.PLAYSTATE_PLAYING); Log.w(TAG, "play song"); } public void pausePlayer() { if (serviceState != ServiceState.Paused && serviceState != ServiceState.Playing) return; player.pause(); serviceState = ServiceState.Paused; notification.notifyPaused(true); // Updates Lock-Screen Widget if (lockscreenController != null) lockscreenController.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); broadcastState(ServicePlayMusic.BROADCAST_EXTRA_PAUSED); } public void unpausePlayer() { if (serviceState != ServiceState.Paused && serviceState != ServiceState.Playing) return; player.start(); serviceState = ServiceState.Playing; notification.notifyPaused(false); // Updates Lock-Screen Widget if (lockscreenController != null) lockscreenController.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); broadcastState(ServicePlayMusic.BROADCAST_EXTRA_UNPAUSED); } /** * Toggles between Pause and Unpause. * * @see pausePlayer() * @see unpausePlayer() */ public void togglePlayback() { if (serviceState == ServiceState.Paused) unpausePlayer(); else pausePlayer(); } public void seekTo(int position) { player.seekTo(position); } /** * Toggles the Shuffle mode * (if will play songs in random order). */ public void toggleShuffle() { shuffleMode = !shuffleMode; } /** * Shuffle mode state. * @return If Shuffle mode is on/off. */ public boolean isShuffle() { return shuffleMode; } /** * Toggles the Repeat mode * (if the current song will play again * when completed). */ public void toggleRepeat() { repeatMode = ! repeatMode; } /** * Repeat mode state. * @return If Repeat mode is on/off. */ public boolean isRepeat() { return repeatMode; } // THESE ARE METHODS RELATED TO CONNECTING THE SERVICE // TO THE ANDROID PLATFORM // NOTHING TO DO WITH MUSIC-PLAYING /** * Tells if this service is bound to an Activity. */ public boolean musicBound = false; /** * Defines the interaction between an Activity and this Service. */ public class MusicBinder extends Binder { public ServicePlayMusic getService() { return ServicePlayMusic.this; } } /** * Token for the interaction between an Activity and this Service. */ private final IBinder musicBind = new MusicBinder(); /** * Called when the Service is finally bound to the app. */ @Override public IBinder onBind(Intent intent) { return musicBind; } /** * Called when the Service is unbound - user quitting * the app or something. */ @Override public boolean onUnbind(Intent intent) { return false; } /** * Sorts the internal Now Playing List according to * a `rule`. * * Supported ways to sort are: * - "title": Sorts alphabetically by song title * - "artist": Sorts alphabetically by artist name * - "album": Sorts alphabetically by album name * - "track": Sorts by track number * - "random": Sorts randomly (shuffles song's orders) */ public void sortBy(String rule) { // We track the currently playing song to // a position on the song list. // // When we sort, it'll be on a different // position. // // So we keep a reference to the currently // playing song's ID and then look it up // after sorting. long nowPlayingSongID = ((currentSong == null) ? 0 : currentSong.getId()); if (rule.equals("title")) Collections.sort(songs, new Comparator<Song>() { public int compare(Song a, Song b) { return a.getTitle().compareTo(b.getTitle()); } }); else if (rule.equals("artist")) Collections.sort(songs, new Comparator<Song>() { public int compare(Song a, Song b) { return a.getArtist().compareTo(b.getArtist()); } }); else if (rule.equals("album")) Collections.sort(songs, new Comparator<Song>() { public int compare(Song a, Song b) { return a.getAlbum().compareTo(b.getAlbum()); } }); else if (rule.equals("track")) Collections.sort(songs, new Comparator<Song>() { public int compare(Song a, Song b) { int left = a.getTrackNumber(); int right = b.getTrackNumber(); if (left == right) return 0; return ((left < right) ? -1 : 1); } }); else if (rule.equals("random")) { Collections.shuffle(songs, randomNumberGenerator); } // Now that we sorted, get again the current song // position. int position = 0; for (Song song : songs) { if (song.getId() == nowPlayingSongID) { currentSongPosition = position; break; } position++; } } /** * Returns the song on the Now Playing List at `position`. */ public Song getSong(int position) { return songs.get(position); } /** * Displays a notification on the status bar with the * current song and some nice buttons. */ public void notifyCurrentSong() { if (! kMP.settings.get("show_notification", true)) return; if (currentSong == null) return; if (notification == null) notification = new NotificationMusic(); notification.notifySong(this, this, currentSong); } /** * Disables the ability to notify things on the * status bar. * * @see #notifyCurrentSong() */ public void cancelNotification() { if (notification == null) return; notification.cancel(); notification = null; } /** * Shouts the state of the Music Service. * * @note This broadcast is visible only inside this application. * * @note Will get received by listeners of `ServicePlayMusic.BROADCAST_ACTION` * * @param state Current state of the Music Service. */ private void broadcastState(String state) { if (currentSong == null) return; Intent broadcastIntent = new Intent(ServicePlayMusic.BROADCAST_ACTION); broadcastIntent.putExtra(ServicePlayMusic.BROADCAST_EXTRA_STATE, state); broadcastIntent.putExtra(ServicePlayMusic.BROADCAST_EXTRA_SONG_ID, currentSong.getId()); LocalBroadcastManager .getInstance(getApplicationContext()) .sendBroadcast(broadcastIntent); Log.w(TAG, "sentBroadcast"); } }