From 3bdecbebe45e3f96d4f45913697e9899fe9eaa68 Mon Sep 17 00:00:00 2001 From: Karim Abou Zeid Date: Fri, 19 Jun 2015 02:29:44 +0200 Subject: [PATCH] Cleaned up and rewrote big parts of the MusicService. Added gapless playback option in settings. --- .../appwidget/MusicPlayerWidget.java | 5 +- .../gramophone/helper/MusicPlayerRemote.java | 48 +- .../helper/PlayingNotificationHelper.java | 1 + .../service/MediaButtonIntentReceiver.java | 2 +- .../gramophone/service/MultiPlayer.java | 260 +++++++++++ .../gramophone/service/MusicService.java | 410 ++++++++---------- .../activities/MusicControllerActivity.java | 3 +- .../gramophone/util/PreferenceUtils.java | 7 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/pref_audio.xml | 8 + 10 files changed, 473 insertions(+), 273 deletions(-) create mode 100644 app/src/main/java/com/kabouzeid/gramophone/service/MultiPlayer.java diff --git a/app/src/main/java/com/kabouzeid/gramophone/appwidget/MusicPlayerWidget.java b/app/src/main/java/com/kabouzeid/gramophone/appwidget/MusicPlayerWidget.java index 5d28480b..38db31c5 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/appwidget/MusicPlayerWidget.java +++ b/app/src/main/java/com/kabouzeid/gramophone/appwidget/MusicPlayerWidget.java @@ -36,11 +36,10 @@ public class MusicPlayerWidget extends AppWidgetProvider { } public static void updateWidgets(final Context context, final Song song, boolean isPlaying) { + if (song.id == -1) return; widgetLayout = new RemoteViews(context.getPackageName(), R.layout.music_player_widget); linkButtons(context, widgetLayout); - if (song.id != -1) { - widgetLayout.setTextViewText(R.id.song_title, song.title); - } + widgetLayout.setTextViewText(R.id.song_title, song.title); updateWidgetsPlayState(context, isPlaying); loadAlbumArt(context, song); } diff --git a/app/src/main/java/com/kabouzeid/gramophone/helper/MusicPlayerRemote.java b/app/src/main/java/com/kabouzeid/gramophone/helper/MusicPlayerRemote.java index 6126f650..cdf6bb61 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/helper/MusicPlayerRemote.java +++ b/app/src/main/java/com/kabouzeid/gramophone/helper/MusicPlayerRemote.java @@ -45,7 +45,7 @@ public class MusicPlayerRemote { MusicService.MusicBinder binder = (MusicService.MusicBinder) service; musicService = binder.getService(); musicService.restorePreviousState(restoredOriginalQueue, playingQueue, position); - if(startAfterConnected) resumePlaying(); + if (startAfterConnected) resumePlaying(); } @Override @@ -78,7 +78,7 @@ public class MusicPlayerRemote { public static void pauseSong() { if (musicService != null) { - musicService.pausePlaying(false); + musicService.pause(false); } } @@ -101,23 +101,12 @@ public class MusicPlayerRemote { } public static boolean isPlaying() { - return musicService != null && musicService.isPlaying(); + return musicService != null && musicService.isPlayingAndNotFadingDown(); } public static void resumePlaying() { if (musicService != null) { - musicService.resumePlaying(false); - } - } - - public static long getCurrentSongId() { - if (musicService != null) { - return musicService.getCurrentSongId(); - } - try { - return playingQueue.get(position).id; - } catch (Exception e) { - return -1; + musicService.play(false); } } @@ -131,12 +120,12 @@ public class MusicPlayerRemote { } public static Song getCurrentSong() { - final int position = getPosition(); - if (position != -1) { - try { - return getPlayingQueue().get(position); - } catch (Exception ignored) { - } + if (musicService != null) { + return musicService.getCurrentSong(); + } + try { + return getPlayingQueue().get(getPosition()); + } catch (Exception ignored) { } return new Song(); } @@ -148,13 +137,6 @@ public class MusicPlayerRemote { return position; } - private static void setPosition(int position) { - MusicPlayerRemote.position = position; - if (musicService != null) { - musicService.setPosition(position); - } - } - public static ArrayList getPlayingQueue() { if (musicService != null) { playingQueue = musicService.getPlayingQueue(); @@ -163,18 +145,14 @@ public class MusicPlayerRemote { } public static int getSongProgressMillis() { - if (isPlayerPrepared()) { + if (musicService != null) { return musicService.getSongProgressMillis(); } return -1; } - public static boolean isPlayerPrepared() { - return musicService != null && musicService.isPlayerPrepared(); - } - public static int getSongDurationMillis() { - if (isPlayerPrepared()) { + if (musicService != null) { return musicService.getSongDurationMillis(); } return -1; @@ -182,7 +160,7 @@ public class MusicPlayerRemote { public static void seekTo(int millis) { if (musicService != null) { - musicService.seekTo(millis); + musicService.seek(millis); } } diff --git a/app/src/main/java/com/kabouzeid/gramophone/helper/PlayingNotificationHelper.java b/app/src/main/java/com/kabouzeid/gramophone/helper/PlayingNotificationHelper.java index d4e82998..9bfd2f1c 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/helper/PlayingNotificationHelper.java +++ b/app/src/main/java/com/kabouzeid/gramophone/helper/PlayingNotificationHelper.java @@ -83,6 +83,7 @@ public class PlayingNotificationHelper { } private void buildNotification(final Song song, final boolean isPlaying, final boolean isColored) { + if (song.id == -1) return; this.isColored = isColored; currentSong = song; this.isPlaying = isPlaying; diff --git a/app/src/main/java/com/kabouzeid/gramophone/service/MediaButtonIntentReceiver.java b/app/src/main/java/com/kabouzeid/gramophone/service/MediaButtonIntentReceiver.java index b8fe931b..96b618cc 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/service/MediaButtonIntentReceiver.java +++ b/app/src/main/java/com/kabouzeid/gramophone/service/MediaButtonIntentReceiver.java @@ -31,7 +31,7 @@ public class MediaButtonIntentReceiver extends BroadcastReceiver { command = MusicService.ACTION_TOGGLE_PLAYBACK; break; case KeyEvent.KEYCODE_MEDIA_PLAY: - command = MusicService.ACTION_RESUME; + command = MusicService.ACTION_PLAY; break; case KeyEvent.KEYCODE_MEDIA_NEXT: command = MusicService.ACTION_SKIP; diff --git a/app/src/main/java/com/kabouzeid/gramophone/service/MultiPlayer.java b/app/src/main/java/com/kabouzeid/gramophone/service/MultiPlayer.java new file mode 100644 index 00000000..00fc8852 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/gramophone/service/MultiPlayer.java @@ -0,0 +1,260 @@ +package com.kabouzeid.gramophone.service; + +import android.content.Intent; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.audiofx.AudioEffect; +import android.net.Uri; +import android.os.Handler; +import android.os.PowerManager; +import android.util.Log; +import android.widget.Toast; + +import com.kabouzeid.gramophone.R; +import com.kabouzeid.gramophone.util.PreferenceUtils; + +import java.io.IOException; +import java.lang.ref.WeakReference; + +public final class MultiPlayer implements MediaPlayer.OnErrorListener, + MediaPlayer.OnCompletionListener { + public static final String TAG = MultiPlayer.class.getSimpleName(); + + private final WeakReference mService; + + private MediaPlayer mCurrentMediaPlayer = new MediaPlayer(); + + private MediaPlayer mNextMediaPlayer; + + private Handler mHandler; + + private boolean mIsInitialized = false; + + /** + * Constructor of MultiPlayer + */ + public MultiPlayer(final MusicService service) { + mService = new WeakReference<>(service); + mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK); + } + + /** + * @param path The path of the file, or the http/rtsp URL of the stream + * you want to play + */ + public void setDataSource(final String path) { + mIsInitialized = false; + mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path); + if (mIsInitialized) { + setNextDataSource(null); + } + } + + /** + * @param player The {@link MediaPlayer} to use + * @param path The path of the file, or the http/rtsp URL of the stream + * you want to play + * @return True if the player has been prepared and is + * ready to play, false otherwise + */ + private boolean setDataSourceImpl(final MediaPlayer player, final String path) { + try { + player.reset(); + player.setOnPreparedListener(null); + if (path.startsWith("content://")) { + player.setDataSource(mService.get(), Uri.parse(path)); + } else { + player.setDataSource(path); + } + player.setAudioStreamType(AudioManager.STREAM_MUSIC); + player.prepare(); + } catch (final IOException e) { + return false; + } catch (final IllegalArgumentException e) { + return false; + } + player.setOnCompletionListener(this); + player.setOnErrorListener(this); + final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); + intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId()); + intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, mService.get().getPackageName()); + intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC); + mService.get().sendBroadcast(intent); + return true; + } + + /** + * Set the MediaPlayer to start when this MediaPlayer finishes playback. + * + * @param path The path of the file, or the http/rtsp URL of the stream + * you want to play + */ + public void setNextDataSource(final String path) { + try { + mCurrentMediaPlayer.setNextMediaPlayer(null); + } catch (IllegalArgumentException e) { + Log.i(TAG, "Next media player is current one, continuing"); + } catch (IllegalStateException e) { + Log.e(TAG, "Media player not initialized!"); + return; + } + if (mNextMediaPlayer != null) { + mNextMediaPlayer.release(); + mNextMediaPlayer = null; + } + if (path == null) { + return; + } + if (PreferenceUtils.getInstance(mService.get()).gaplessPlayback()) { + mNextMediaPlayer = new MediaPlayer(); + mNextMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK); + mNextMediaPlayer.setAudioSessionId(getAudioSessionId()); + if (setDataSourceImpl(mNextMediaPlayer, path)) { + mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer); + } else { + if (mNextMediaPlayer != null) { + mNextMediaPlayer.release(); + mNextMediaPlayer = null; + } + } + } + } + + /** + * Sets the handler + * + * @param handler The handler to use + */ + public void setHandler(final Handler handler) { + mHandler = handler; + } + + /** + * @return True if the player is ready to go, false otherwise + */ + public boolean isInitialized() { + return mIsInitialized; + } + + /** + * Starts or resumes playback. + */ + public void start() { + mCurrentMediaPlayer.start(); + } + + /** + * Resets the MediaPlayer to its uninitialized state. + */ + public void stop() { + mCurrentMediaPlayer.reset(); + mIsInitialized = false; + } + + /** + * Releases resources associated with this MediaPlayer object. + */ + public void release() { + stop(); + mCurrentMediaPlayer.release(); + } + + /** + * Pauses playback. Call start() to resume. + */ + public void pause() { + mCurrentMediaPlayer.pause(); + } + + /** + * Checks whether the MultiPlayer is playing. + */ + public boolean isPlaying() { + return mIsInitialized && mCurrentMediaPlayer.isPlaying(); + } + + /** + * Gets the duration of the file. + * + * @return The duration in milliseconds + */ + public int duration() { + return mCurrentMediaPlayer.getDuration(); + } + + /** + * Gets the current playback position. + * + * @return The current position in milliseconds + */ + public int position() { + return mCurrentMediaPlayer.getCurrentPosition(); + } + + /** + * Gets the current playback position. + * + * @param whereto The offset in milliseconds from the start to seek to + * @return The offset in milliseconds from the start to seek to + */ + public long seek(final long whereto) { + mCurrentMediaPlayer.seekTo((int) whereto); + return whereto; + } + + /** + * Sets the volume on this player. + * + * @param vol Left and right volume scalar + */ + public void setVolume(final float vol) { + mCurrentMediaPlayer.setVolume(vol, vol); + } + + /** + * Sets the audio session ID. + * + * @param sessionId The audio session ID + */ + public void setAudioSessionId(final int sessionId) { + mCurrentMediaPlayer.setAudioSessionId(sessionId); + } + + /** + * Returns the audio session ID. + * + * @return The current audio session ID. + */ + public int getAudioSessionId() { + return mCurrentMediaPlayer.getAudioSessionId(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onError(final MediaPlayer mp, final int what, final int extra) { + Toast.makeText(mService.get().getApplicationContext(), mService.get().getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show(); + mService.get().playNextSong(true); + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public void onCompletion(final MediaPlayer mp) { + if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) { + mIsInitialized = false; + mCurrentMediaPlayer.release(); + mCurrentMediaPlayer = mNextMediaPlayer; + mIsInitialized = true; + mNextMediaPlayer = null; + mHandler.sendEmptyMessage(MusicService.TRACK_WENT_TO_NEXT); + } else { + mService.get().acquireWakeLock(30000); + mHandler.sendEmptyMessage(MusicService.TRACK_ENDED); + mHandler.sendEmptyMessage(MusicService.RELEASE_WAKELOCK); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java b/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java index d8a84498..298298e3 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java +++ b/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java @@ -11,10 +11,8 @@ import android.content.IntentFilter; import android.graphics.Bitmap; import android.media.AudioManager; import android.media.MediaMetadataRetriever; -import android.media.MediaPlayer; import android.media.RemoteControlClient; import android.media.audiofx.AudioEffect; -import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Handler; @@ -50,15 +48,12 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; -public class MusicService extends Service implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener { +public class MusicService extends Service { public static final String PHONOGRAPH_PACKAGE_NAME = "com.kabouzeid.gramophone"; public static final String MUSIC_PACKAGE_NAME = "com.android.music"; - public static final int NOTIFICATION_ID = 1337; - public static final String ACTION_TOGGLE_PLAYBACK = "com.kabouzeid.gramophone.action.TOGGLE_PLAYBACK"; public static final String ACTION_PLAY = "com.kabouzeid.gramophone.action.PLAY"; - public static final String ACTION_RESUME = "com.kabouzeid.gramophone.action.RESUME"; public static final String ACTION_PAUSE = "com.kabouzeid.gramophone.action.PAUSE"; public static final String ACTION_STOP = "com.kabouzeid.gramophone.action.STOP"; public static final String ACTION_SKIP = "com.kabouzeid.gramophone.action.SKIP"; @@ -71,11 +66,15 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe public static final String SHUFFLEMODE_CHANGED = "com.kabouzeid.gramophone.shufflemodechanged"; public static final String POSITION_IN_SONG_CHANGED = "com.kabouzeid.phonograph.positionchanged"; - private static final int FOCUSCHANGE = 5; + private static final int FOCUS_CHANGE = 5; private static final int DUCK = 6; private static final int UNDUCK = 7; - private static final int FADEDOWNANDPAUSE = 8; - private static final int FADEUPANDRESUME = 9; + private static final int FADE_DOWN_AND_PAUSE = 8; + private static final int FADE_UP_AND_RESUME = 9; + public static final int RELEASE_WAKELOCK = 10; + public static final int TRACK_ENDED = 11; + public static final int TRACK_WENT_TO_NEXT = 12; + public static final int PLAY_SONG = 13; public static final int SHUFFLE_MODE_NONE = 0; public static final int SHUFFLE_MODE_SHUFFLE = 1; @@ -86,14 +85,13 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe private static final String TAG = MusicService.class.getSimpleName(); private final IBinder musicBind = new MusicBinder(); - private MediaPlayer player; + private MultiPlayer player; private ArrayList playingQueue; private ArrayList originalPlayingQueue; - private int currentSongId = -1; private int position = -1; + private int nextPosition = -1; private int shuffleMode; private int repeatMode; - private boolean isPlayerPrepared; private boolean pausedByTransientLossOfFocus; private boolean thingsRegistered; private boolean saveQueuesAgain; @@ -104,15 +102,15 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe private PowerManager.WakeLock wakeLock; private String currentAlbumArtUri; private MusicPlayerHandler playerHandler; - private boolean fadingDown = false; + private boolean isFadingDown = false; private HandlerThread handlerThread; private final BroadcastReceiver becomingNoisyReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().compareTo(AudioManager.ACTION_AUDIO_BECOMING_NOISY) == 0) { - pausePlaying(true); - pausePlaying(false); + pause(true); + pause(false); } } }; @@ -120,14 +118,13 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe private final AudioManager.OnAudioFocusChangeListener audioFocusListener = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(final int focusChange) { - playerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget(); + playerHandler.obtainMessage(FOCUS_CHANGE, focusChange, 0).sendToTarget(); } }; @Override public void onCreate() { super.onCreate(); - isPlayerPrepared = false; playingQueue = new ArrayList<>(); originalPlayingQueue = new ArrayList<>(); playingNotificationHelper = new PlayingNotificationHelper(this); @@ -144,6 +141,9 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe handlerThread.start(); playerHandler = new MusicPlayerHandler(this, handlerThread.getLooper()); + player = new MultiPlayer(this); + player.setHandler(playerHandler); + registerEverything(); } @@ -183,26 +183,22 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe @Override public int onStartCommand(Intent intent, int flags, int startId) { - setUpMediaPlayerIfNeeded(); if (intent != null) { if (intent.getAction() != null) { String action = intent.getAction(); switch (action) { case ACTION_TOGGLE_PLAYBACK: - if (isPlaying()) { - pausePlaying(false); + if (isPlayingAndNotFadingDown()) { + pause(false); } else { - resumePlaying(false); + play(false); } break; case ACTION_PAUSE: - pausePlaying(false); + pause(false); break; case ACTION_PLAY: - playSong(); - break; - case ACTION_RESUME: - resumePlaying(false); + play(false); break; case ACTION_REWIND: back(true); @@ -211,7 +207,7 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe playNextSong(true); break; case ACTION_STOP: - stopPlaying(); + stop(); break; case ACTION_QUIT: killEverythingAndReleaseResources(); @@ -260,33 +256,22 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe } private void killEverythingAndReleaseResources() { - stopPlaying(); + stop(); playingNotificationHelper.killNotification(); savePosition(); saveQueues(); stopSelf(); } - public void stopPlaying() { + public void stop() { pausedByTransientLossOfFocus = false; - isPlayerPrepared = false; - if (player != null) { - player.stop(); - player.release(); - player = null; - } + player.stop(); + player.release(); notifyChange(PLAYSTATE_CHANGED); } - public boolean isPlaying() { - return isPlaying(false); - } - - private boolean isPlaying(boolean doNotConsiderFadingDown) { - if (doNotConsiderFadingDown) - return player != null && isPlayerPrepared && player.isPlaying(); - else - return player != null && isPlayerPrepared && player.isPlaying() && !fadingDown; + public boolean isPlayingAndNotFadingDown() { + return player.isPlaying() && !isFadingDown; } public void saveQueues() { @@ -315,59 +300,33 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe return position; } - public void setPosition(int position) { + private void setPosition(int position) { this.position = position; } - @Override - public void onCompletion(MediaPlayer mp) { - if (isLastTrack() && getRepeatMode() == REPEAT_MODE_NONE) { - notifyChange(PLAYSTATE_CHANGED); - } else { - acquireWakeLock(30000); - playNextSong(false); - } - } - public void playNextSong(boolean force) { - if (position != -1) { - if (isPlayerPrepared) { - setPosition(getNextPosition(force)); - playSong(); - } + playSongAt(getNextPosition(force)); + } + + private void openTrackAndPrepareNextAt(int position) { + synchronized (this) { + setPosition(position); + openCurrent(); + prepareNext(); + notifyChange(META_CHANGED); } } - public void playSong() { - if (requestFocus()) { - setUpMediaPlayerIfNeeded(); - registerEverything(); - isPlayerPrepared = false; - player.reset(); - if (position != -1) { - try { - Uri trackUri = getCurrentPositionTrackUri(); - player.setAudioStreamType(AudioManager.STREAM_MUSIC); - player.setDataSource(getApplicationContext(), trackUri); - player.prepareAsync(); - } catch (Exception ignored) { - // handled in onError() - } - currentSongId = playingQueue.get(getPosition()).id; - notifyChange(META_CHANGED); - } else { - notifyChange(PLAYSTATE_CHANGED); - } + private void openCurrent() { + synchronized (this) { + player.setDataSource(getTrackUri(getCurrentSong())); } } - private void openAudioEffectSession() { - if (player != null) { - final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); - intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, getAudioSessionId()); - intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); - intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC); - sendBroadcast(intent); + private void prepareNext() { + synchronized (this) { + nextPosition = getNextPosition(false); + player.setNextDataSource(getTrackUri(getSongAt(nextPosition))); } } @@ -426,49 +385,40 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe .apply(); } - private void setUpMediaPlayerIfNeeded() { - if (player == null) { - player = new MediaPlayer(); + public Song getCurrentSong() { + return getSongAt(getPosition()); + } - player.setOnPreparedListener(this); - player.setOnCompletionListener(this); - player.setOnErrorListener(this); - - player.setAudioStreamType(AudioManager.STREAM_MUSIC); - player.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); + public Song getSongAt(int position) { + if (position >= 0 && position < getPlayingQueue().size()) { + return getPlayingQueue().get(position); + } else { + return new Song(); } } private void updateNotification() { - playingNotificationHelper.buildNotification(playingQueue.get(position), isPlaying()); + playingNotificationHelper.buildNotification(getCurrentSong(), isPlayingAndNotFadingDown()); } private void updateWidgets() { - MusicPlayerWidget.updateWidgets(this, playingQueue.get(position), isPlaying()); + MusicPlayerWidget.updateWidgets(this, getCurrentSong(), isPlayingAndNotFadingDown()); } - private Uri getCurrentPositionTrackUri() { - return ContentUris.withAppendedId(android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, playingQueue.get(position).id); + private static String getTrackUri(Song song) { + return ContentUris.withAppendedId(android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, song.id).toString(); } public int getNextPosition(boolean force) { - int position = 0; + int position = getPosition() + 1; switch (repeatMode) { - case REPEAT_MODE_NONE: - position = getPosition() + 1; - if (isLastTrack()) { - position -= 1; - } - break; case REPEAT_MODE_ALL: - position = getPosition() + 1; if (isLastTrack()) { position = 0; } break; case REPEAT_MODE_THIS: if (force) { - position = getPosition() + 1; if (isLastTrack()) { position = 0; } @@ -476,6 +426,12 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe position = getPosition(); } break; + default: + case REPEAT_MODE_NONE: + if (isLastTrack()) { + position -= 1; + } + break; } return position; } @@ -501,41 +457,24 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe PreferenceManager.getDefaultSharedPreferences(this).edit() .putInt(AppKeys.SP_REPEAT_MODE, repeatMode) .apply(); + prepareNext(); notifyChange(REPEATMODE_CHANGED); break; } } - @Override - public boolean onError(MediaPlayer mp, int what, int extra) { - isPlayerPrepared = false; - player.reset(); - player = null; - notifyChange(PLAYSTATE_CHANGED); - Toast.makeText(getApplicationContext(), getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show(); - return false; - } - - @Override - public void onPrepared(MediaPlayer mp) { - isPlayerPrepared = true; - openAudioEffectSession(); - resumePlaying(false); - savePosition(); - releaseWakeLock(); - } - public void openQueue(final ArrayList playingQueue, final int startPosition, final boolean startPlaying) { if (playingQueue != null && !playingQueue.isEmpty() && startPosition >= 0 && startPosition < playingQueue.size()) { originalPlayingQueue = playingQueue; this.playingQueue = new ArrayList<>(originalPlayingQueue); - setPosition(startPosition); + + int position = startPosition; if (shuffleMode == SHUFFLE_MODE_SHUFFLE) { ShuffleHelper.makeShuffleList(this.playingQueue, startPosition); - setPosition(0); + position = 0; } if (startPlaying) { - playSong(); + playSongAt(position); } saveState(); } @@ -612,9 +551,6 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe for (int i = 0; i < originalPlayingQueue.size(); i++) { if (originalPlayingQueue.get(i).id == song.id) originalPlayingQueue.remove(i); } - if (song.id == currentSongId) { - playSong(); - } saveState(); } @@ -633,59 +569,60 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe } else if (from == currentPosition) { setPosition(to); } + if (from != to) prepareNext(); saveState(); } - public long getCurrentSongId() { - return currentSongId; - } - public void playSongAt(final int position) { - if (position < getPlayingQueue().size() && position >= 0) { - setPosition(position); - playSong(); - } else { - Log.e(TAG, "No song in queue at given index!"); - } + // handle this on the handlers thread to avoid blocking the ui thread + playerHandler.removeMessages(PLAY_SONG); + playerHandler.obtainMessage(PLAY_SONG, position, 0).sendToTarget(); } - public void pausePlaying(boolean forceNoFading) { + private void playSongAtImpl(int position) { + openTrackAndPrepareNextAt(position); + play(false); + } + + public void pause(boolean forceNoFading) { pausedByTransientLossOfFocus = false; - if (!forceNoFading && PreferenceUtils.getInstance(this).fadePlayPauseAndInterruptions()) { - playerHandler.removeMessages(FADEUPANDRESUME); - playerHandler.sendEmptyMessage(FADEDOWNANDPAUSE); + if (!forceNoFading && PreferenceUtils.getInstance(this).fadePlayPause()) { + playerHandler.removeMessages(FADE_UP_AND_RESUME); + playerHandler.sendEmptyMessage(FADE_DOWN_AND_PAUSE); } else { - pause(); + pauseImpl(); } } - private void pause() { - playerHandler.removeMessages(FADEUPANDRESUME); - if (isPlaying(true)) { + private void pauseImpl() { + playerHandler.removeMessages(FADE_UP_AND_RESUME); + if (player.isPlaying()) { player.pause(); notifyChange(PLAYSTATE_CHANGED); } } - public void resumePlaying(boolean forceNoFading) { - if (!forceNoFading && PreferenceUtils.getInstance(this).fadePlayPauseAndInterruptions()) { - playerHandler.removeMessages(FADEDOWNANDPAUSE); - playerHandler.sendEmptyMessage(FADEUPANDRESUME); + public void play(boolean forceNoFading) { + if (!forceNoFading && PreferenceUtils.getInstance(this).fadePlayPause()) { + playerHandler.removeMessages(FADE_DOWN_AND_PAUSE); + playerHandler.sendEmptyMessage(FADE_UP_AND_RESUME); } else { - if (player != null) player.setVolume(1f, 1f); - resume(); + if (player != null) player.setVolume(1f); + playImpl(); } } - private void resume() { - playerHandler.removeMessages(FADEDOWNANDPAUSE); - if (!isPlaying(true)) { + private void playImpl() { + synchronized (this) { + playerHandler.removeMessages(FADE_DOWN_AND_PAUSE); if (requestFocus()) { - if (isPlayerPrepared()) { - player.start(); - notifyChange(PLAYSTATE_CHANGED); - } else { - playSong(); + if (!player.isPlaying()) { + if (!player.isInitialized()) { + playSongAt(getPosition()); + } else { + player.start(); + notifyChange(PLAYSTATE_CHANGED); + } } } else { Toast.makeText(this, getResources().getString(R.string.audio_focus_denied), Toast.LENGTH_SHORT).show(); @@ -694,65 +631,54 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe } public void playPreviousSong(boolean force) { - if (position != -1) { - setPosition(getPreviousPosition(force)); - playSong(); - } + playSongAt(getPreviousPosition(force)); } public void back(boolean force) { - if (position != -1) { - if (isPlayerPrepared() && getSongProgressMillis() > 2000) { - seekTo(0); - } else { - playPreviousSong(force); - } + if (getSongProgressMillis() > 2000) { + seek(0); + } else { + playPreviousSong(force); } } public int getPreviousPosition(boolean force) { - int position = 0; + int newPosition = getPosition() - 1; switch (repeatMode) { - case REPEAT_MODE_NONE: - position = getPosition() - 1; - if (position < 0) { - position = 0; - } - break; case REPEAT_MODE_ALL: - position = getPosition() - 1; - if (position < 0) { - position = getPlayingQueue().size() - 1; + if (newPosition < 0) { + newPosition = getPlayingQueue().size() - 1; } break; case REPEAT_MODE_THIS: if (force) { - position = getPosition() - 1; - if (position < 0) { - position = getPlayingQueue().size() - 1; + if (newPosition < 0) { + newPosition = getPlayingQueue().size() - 1; } } else { - position = getPosition(); + newPosition = getPosition(); + } + break; + default: + case REPEAT_MODE_NONE: + if (newPosition < 0) { + newPosition = 0; } break; } - return position; + return newPosition; } public int getSongProgressMillis() { - return player.getCurrentPosition(); + return player.isInitialized() ? player.position() : 0; } public int getSongDurationMillis() { - return player.getDuration(); + return player.isInitialized() ? player.duration() : 0; } - public void seekTo(int millis) { - player.seekTo(millis); - } - - public boolean isPlayerPrepared() { - return player != null && isPlayerPrepared; + public void seek(int millis) { + player.seek(millis); } public void cycleRepeatMode() { @@ -788,35 +714,37 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe switch (shuffleMode) { case SHUFFLE_MODE_SHUFFLE: this.shuffleMode = shuffleMode; - ShuffleHelper.makeShuffleList(this.playingQueue, getPosition()); + ShuffleHelper.makeShuffleList(this.getPlayingQueue(), getPosition()); setPosition(0); break; case SHUFFLE_MODE_NONE: this.shuffleMode = shuffleMode; + int currentSongId = getCurrentSong().id; playingQueue = new ArrayList<>(originalPlayingQueue); int newPosition = 0; - for (Song song : playingQueue) { + for (Song song : getPlayingQueue()) { if (song.id == currentSongId) { - newPosition = playingQueue.indexOf(song); + newPosition = getPlayingQueue().indexOf(song); } } setPosition(newPosition); break; } + prepareNext(); notifyChange(SHUFFLEMODE_CHANGED); } private void notifyChange(final String what) { final Intent internalIntent = new Intent(what); - final int position = getPosition(); - if (position >= 0 && !playingQueue.isEmpty()) { - final Song currentSong = playingQueue.get(position); + + final Song currentSong = getCurrentSong(); + if (currentSong.id != -1) { internalIntent.putExtra("id", currentSong.id); internalIntent.putExtra("artist", currentSong.artistName); internalIntent.putExtra("album", currentSong.albumName); internalIntent.putExtra("track", currentSong.title); } - internalIntent.putExtra("playing", isPlaying()); + internalIntent.putExtra("playing", isPlayingAndNotFadingDown()); sendStickyBroadcast(internalIntent); //to let other apps know whats playing. i.E. last.fm (scrobbling) or musixmatch @@ -825,7 +753,7 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe sendStickyBroadcast(publicMusicIntent); if (what.equals(PLAYSTATE_CHANGED)) { - final boolean isPlaying = isPlaying(); + final boolean isPlaying = isPlayingAndNotFadingDown(); playingNotificationHelper.updatePlayState(isPlaying); MusicPlayerWidget.updateWidgetsPlayState(this, isPlaying); remoteControlClient.setPlaybackState(isPlaying ? RemoteControlClient.PLAYSTATE_PLAYING : RemoteControlClient.PLAYSTATE_PAUSED); @@ -842,13 +770,13 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe return player.getAudioSessionId(); } - private void releaseWakeLock() { + public void releaseWakeLock() { if (wakeLock.isHeld()) { wakeLock.release(); } } - private void acquireWakeLock(long milli) { + public void acquireWakeLock(long milli) { wakeLock.acquire(milli); } @@ -884,7 +812,7 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe currentDuckVolume = .2f; } if (service.player != null) - service.player.setVolume(currentDuckVolume, currentDuckVolume); + service.player.setVolume(currentDuckVolume); break; case UNDUCK: @@ -895,48 +823,66 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe currentDuckVolume = 1.0f; } if (service.player != null) - service.player.setVolume(currentDuckVolume, currentDuckVolume); + service.player.setVolume(currentDuckVolume); break; - case FADEDOWNANDPAUSE: - if (!service.fadingDown) { - service.fadingDown = true; + case FADE_DOWN_AND_PAUSE: + if (!service.isFadingDown) { + service.isFadingDown = true; service.notifyChange(PLAYSTATE_CHANGED); } - currentPlayPauseFadeVolume -= .2f; + currentPlayPauseFadeVolume -= .125f; if (currentPlayPauseFadeVolume > 0f) { - sendEmptyMessageDelayed(FADEDOWNANDPAUSE, 10); + sendEmptyMessageDelayed(FADE_DOWN_AND_PAUSE, 10); } else { currentPlayPauseFadeVolume = 0f; - service.fadingDown = false; - service.pausePlaying(true); + service.isFadingDown = false; + service.pause(true); } if (service.player != null) - service.player.setVolume(currentPlayPauseFadeVolume, currentPlayPauseFadeVolume); + service.player.setVolume(currentPlayPauseFadeVolume); break; - case FADEUPANDRESUME: - if (service.fadingDown) { - service.fadingDown = false; + case FADE_UP_AND_RESUME: + if (service.isFadingDown) { + service.isFadingDown = false; service.notifyChange(PLAYSTATE_CHANGED); } - service.resume(); - currentPlayPauseFadeVolume += .2f; + service.playImpl(); + currentPlayPauseFadeVolume += .125f; if (currentPlayPauseFadeVolume < 1.0f) { - sendEmptyMessageDelayed(FADEUPANDRESUME, 10); + sendEmptyMessageDelayed(FADE_UP_AND_RESUME, 10); } else { currentPlayPauseFadeVolume = 1.0f; } if (service.player != null) - service.player.setVolume(currentPlayPauseFadeVolume, currentPlayPauseFadeVolume); + service.player.setVolume(currentPlayPauseFadeVolume); break; - case FOCUSCHANGE: + case TRACK_WENT_TO_NEXT: + service.setPosition(service.nextPosition); + service.prepareNext(); + service.notifyChange(META_CHANGED); + break; + + case TRACK_ENDED: + service.playNextSong(false); + break; + + case RELEASE_WAKELOCK: + service.releaseWakeLock(); + break; + + case PLAY_SONG: + service.playSongAtImpl(msg.arg1); + break; + + case FOCUS_CHANGE: switch (msg.arg1) { case AudioManager.AUDIOFOCUS_GAIN: service.registerEverything(); - if (!service.isPlaying() && service.pausedByTransientLossOfFocus) { - service.resumePlaying(false); + if (!service.isPlayingAndNotFadingDown() && service.pausedByTransientLossOfFocus) { + service.play(false); } removeMessages(DUCK); sendEmptyMessage(UNDUCK); @@ -944,7 +890,7 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe case AudioManager.AUDIOFOCUS_LOSS: // Lost focus for an unbounded amount of time: stop playback and release media player - service.pausePlaying(true); + service.pause(true); service.unregisterEverything(); break; @@ -952,8 +898,8 @@ public class MusicService extends Service implements MediaPlayer.OnPreparedListe // Lost focus for a short time, but we have to stop // playback. We don't release the media player because playback // is likely to resume - service.pausePlaying(false); - service.pausedByTransientLossOfFocus = service.isPlaying(); + service.pause(false); + service.pausedByTransientLossOfFocus = service.isPlayingAndNotFadingDown(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/activities/MusicControllerActivity.java b/app/src/main/java/com/kabouzeid/gramophone/ui/activities/MusicControllerActivity.java index e213748c..4529906f 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/ui/activities/MusicControllerActivity.java +++ b/app/src/main/java/com/kabouzeid/gramophone/ui/activities/MusicControllerActivity.java @@ -329,7 +329,7 @@ public class MusicControllerActivity extends AbsFabActivity { setHeadersText(); setUpAlbumArtAndApplyPalette(); totalSongDuration.setText(MusicUtil.getReadableDurationString(song.duration)); - currentSongProgress.setText(MusicUtil.getReadableDurationString(-1)); + currentSongProgress.setText(MusicUtil.getReadableDurationString(0)); } private void setHeadersText() { @@ -467,6 +467,7 @@ public class MusicControllerActivity extends AbsFabActivity { progressSlider.setMax(totalMillis); progressSlider.setProgress(progressMillis); currentSongProgress.setText(MusicUtil.getReadableDurationString(progressMillis)); + totalSongDuration.setText(MusicUtil.getReadableDurationString(totalMillis)); } }; }); diff --git a/app/src/main/java/com/kabouzeid/gramophone/util/PreferenceUtils.java b/app/src/main/java/com/kabouzeid/gramophone/util/PreferenceUtils.java index 156302a7..6aaaf4f8 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/util/PreferenceUtils.java +++ b/app/src/main/java/com/kabouzeid/gramophone/util/PreferenceUtils.java @@ -42,6 +42,7 @@ public final class PreferenceUtils { public static final String PLAYBACK_CONTROLLER_CARD_NOW_PLAYING = "playback_controller_card_now_playing"; public static final String FADE_PLAY_PAUSE = "fade_play_pause"; public static final String COLORED_NOTIFICATION = "colored_notification"; + public static final String GAPLESS_PLAYBACK = "gapless_playback"; private static PreferenceUtils sInstance; @@ -193,10 +194,14 @@ public final class PreferenceUtils { return mPreferences.getBoolean(ALTERNATIVE_PROGRESS_SLIDER_NOW_PLAYING, false); } - public final boolean fadePlayPauseAndInterruptions() { + public final boolean fadePlayPause() { return mPreferences.getBoolean(FADE_PLAY_PAUSE, true); } + public final boolean gaplessPlayback() { + return mPreferences.getBoolean(GAPLESS_PLAYBACK, true); + } + // public final boolean downloadMissingArtistImages() { // return mPreferences.getBoolean(DOWNLOAD_MISSING_ARTIST_IMAGES, true); // } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f3061eb..e56d9d04 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,6 +109,7 @@ Colored album footers Colored notification Fade play/pause + Gapless playback Force square album art Opaque toolbar Opaque statusbar @@ -143,6 +144,7 @@ "Album footers in the grid are colored with the album cover\'s vibrant color." "The notification is colored with the album cover\'s vibrant color." "Fades the song in/out on play/pause." + "Eliminates the gap between two songs. Disabling this might fix playback issues." Album art in the now playing view is forced to be squared. The toolbar is opaque and do not cover the album art. The statusbar is opaque and do not cover the album art. diff --git a/app/src/main/res/xml/pref_audio.xml b/app/src/main/res/xml/pref_audio.xml index cbbac7fc..13dae0cb 100644 --- a/app/src/main/res/xml/pref_audio.xml +++ b/app/src/main/res/xml/pref_audio.xml @@ -11,6 +11,14 @@ android:summary="@string/pref_summary_fade_play_pause" android:widgetLayout="@layout/preference_dynamic_checkbox" /> + +