diff --git a/app/src/main/java/com/kabouzeid/gramophone/service/MultiPlayer.java b/app/src/main/java/com/kabouzeid/gramophone/service/MultiPlayer.java index e719ac95..9db5a4ff 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/service/MultiPlayer.java +++ b/app/src/main/java/com/kabouzeid/gramophone/service/MultiPlayer.java @@ -1,11 +1,11 @@ package com.kabouzeid.gramophone.service; +import android.content.Context; 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.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -13,32 +13,30 @@ import android.util.Log; import android.widget.Toast; import com.kabouzeid.gramophone.R; +import com.kabouzeid.gramophone.service.Playback.Playback; import com.kabouzeid.gramophone.util.PreferenceUtil; -import java.lang.ref.WeakReference; - /** * @author Andrew Neal, Karim Abou Zeid (kabouzeid) */ -public class MultiPlayer implements MediaPlayer.OnErrorListener, - MediaPlayer.OnCompletionListener { +public class MultiPlayer implements Playback, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener { public static final String TAG = MultiPlayer.class.getSimpleName(); - private final WeakReference mService; + private DoubleMediaPlayer mCurrentMediaPlayer = new DoubleMediaPlayer(); + private DoubleMediaPlayer mNextMediaPlayer; - private MediaPlayer mCurrentMediaPlayer = new MediaPlayer(); - private MediaPlayer mNextMediaPlayer; - - private Handler mHandler; + private Context context; + @Nullable + private Playback.PlaybackCallbacks callbacks; private boolean mIsInitialized = false; /** * Constructor of MultiPlayer */ - public MultiPlayer(final MusicService service) { - mService = new WeakReference<>(service); - mCurrentMediaPlayer.setWakeMode(service, PowerManager.PARTIAL_WAKE_LOCK); + public MultiPlayer(final Context context) { + this.context = context; + mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); } /** @@ -47,6 +45,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, * @return True if the player has been prepared and is * ready to play, false otherwise */ + @Override public boolean setDataSource(@NonNull final String path) { mIsInitialized = false; mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path); @@ -64,15 +63,14 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, * ready to play, false otherwise */ private boolean setDataSourceImpl(@NonNull final MediaPlayer player, @NonNull final String path) { - MusicService service = mService.get(); - if (service == null) { + if (context == null) { return false; } try { player.reset(); player.setOnPreparedListener(null); if (path.startsWith("content://")) { - player.setDataSource(service, Uri.parse(path)); + player.setDataSource(context, Uri.parse(path)); } else { player.setDataSource(path); } @@ -85,9 +83,9 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, 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, service.getPackageName()); + intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName()); intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC); - service.sendBroadcast(intent); + context.sendBroadcast(intent); return true; } @@ -97,9 +95,9 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, * @param path The path of the file, or the http/rtsp URL of the stream * you want to play */ + @Override public void setNextDataSource(@Nullable final String path) { - MusicService service = mService.get(); - if (service == null) { + if (context == null) { return; } try { @@ -117,9 +115,9 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, if (path == null) { return; } - if (PreferenceUtil.getInstance(mService.get()).gaplessPlayback()) { - mNextMediaPlayer = new MediaPlayer(); - mNextMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK); + if (PreferenceUtil.getInstance(context).gaplessPlayback()) { + mNextMediaPlayer = new DoubleMediaPlayer(); + mNextMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); mNextMediaPlayer.setAudioSessionId(getAudioSessionId()); if (setDataSourceImpl(mNextMediaPlayer, path)) { try { @@ -141,17 +139,19 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, } /** - * Sets the handler + * Sets the callbacks * - * @param handler The handler to use + * @param callbacks The callbacks to use */ - public void setHandler(final Handler handler) { - mHandler = handler; + @Override + public void setCallbacks(final Playback.PlaybackCallbacks callbacks) { + this.callbacks = callbacks; } /** * @return True if the player is ready to go, false otherwise */ + @Override public boolean isInitialized() { return mIsInitialized; } @@ -159,6 +159,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, /** * Starts or resumes playback. */ + @Override public boolean start() { try { mCurrentMediaPlayer.start(); @@ -171,6 +172,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, /** * Resets the MediaPlayer to its uninitialized state. */ + @Override public void stop() { mCurrentMediaPlayer.reset(); mIsInitialized = false; @@ -179,6 +181,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, /** * Releases resources associated with this MediaPlayer object. */ + @Override public void release() { stop(); mCurrentMediaPlayer.release(); @@ -190,6 +193,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, /** * Pauses playback. Call start() to resume. */ + @Override public boolean pause() { try { mCurrentMediaPlayer.pause(); @@ -202,6 +206,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, /** * Checks whether the MultiPlayer is playing. */ + @Override public boolean isPlaying() { return mIsInitialized && mCurrentMediaPlayer.isPlaying(); } @@ -211,6 +216,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, * * @return The duration in milliseconds */ + @Override public int duration() { if (!mIsInitialized) { return -1; @@ -227,6 +233,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, * * @return The current position in milliseconds */ + @Override public int position() { if (!mIsInitialized) { return -1; @@ -244,6 +251,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, * @param whereto The offset in milliseconds from the start to seek to * @return The offset in milliseconds from the start to seek to */ + @Override public int seek(final int whereto) { try { mCurrentMediaPlayer.seekTo(whereto); @@ -253,6 +261,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, } } + @Override public boolean setVolume(final float vol) { try { mCurrentMediaPlayer.setVolume(vol, vol); @@ -267,6 +276,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, * * @param sessionId The audio session ID */ + @Override public boolean setAudioSessionId(final int sessionId) { try { mCurrentMediaPlayer.setAudioSessionId(sessionId); @@ -281,6 +291,7 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, * * @return The current audio session ID. */ + @Override public int getAudioSessionId() { return mCurrentMediaPlayer.getAudioSessionId(); } @@ -292,9 +303,11 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, public boolean onError(final MediaPlayer mp, final int what, final int extra) { mIsInitialized = false; mCurrentMediaPlayer.release(); - mCurrentMediaPlayer = new MediaPlayer(); - mCurrentMediaPlayer.setWakeMode(mService.get(), PowerManager.PARTIAL_WAKE_LOCK); - Toast.makeText(mService.get(), mService.get().getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show(); + mCurrentMediaPlayer = new DoubleMediaPlayer(); + mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); + if (context != null) { + Toast.makeText(context, context.getResources().getString(R.string.unplayable_file), Toast.LENGTH_SHORT).show(); + } return false; } @@ -303,21 +316,46 @@ public class MultiPlayer implements MediaPlayer.OnErrorListener, */ @Override public void onCompletion(final MediaPlayer mp) { - MusicService service = mService.get(); - if (service == null) { - return; - } if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) { mIsInitialized = false; mCurrentMediaPlayer.release(); mCurrentMediaPlayer = mNextMediaPlayer; mIsInitialized = true; mNextMediaPlayer = null; - mHandler.sendEmptyMessage(MusicService.TRACK_WENT_TO_NEXT); + if (callbacks != null) + callbacks.onTrackWentToNext(); } else { - service.acquireWakeLock(30000); - mHandler.sendEmptyMessage(MusicService.TRACK_ENDED); - mHandler.sendEmptyMessage(MusicService.RELEASE_WAKELOCK); + if (callbacks != null) + callbacks.onTrackEnded(); + } + } + + private static final class DoubleMediaPlayer extends MediaPlayer implements MediaPlayer.OnCompletionListener { + private MediaPlayer mNextPlayer; + + private OnCompletionListener mCompletion; + + public DoubleMediaPlayer() { + super.setOnCompletionListener(this); + } + + @Override + public void setNextMediaPlayer(final MediaPlayer next) { + mNextPlayer = next; + } + + @Override + public void setOnCompletionListener(final OnCompletionListener listener) { + mCompletion = listener; + } + + @Override + public void onCompletion(final MediaPlayer mp) { + if (mNextPlayer != null) { + // SystemClock.sleep(25); + mNextPlayer.start(); + } + mCompletion.onCompletion(this); } } } \ 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 2aedfb6e..1efc2a8e 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java +++ b/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java @@ -46,6 +46,7 @@ import com.kabouzeid.gramophone.model.Song; import com.kabouzeid.gramophone.provider.HistoryStore; import com.kabouzeid.gramophone.provider.MusicPlaybackQueueStore; import com.kabouzeid.gramophone.provider.SongPlayCountStore; +import com.kabouzeid.gramophone.service.Playback.Playback; import com.kabouzeid.gramophone.util.MusicUtil; import com.kabouzeid.gramophone.util.PreferenceUtil; import com.kabouzeid.gramophone.util.Util; @@ -57,7 +58,7 @@ import java.util.List; /** * @author Karim Abou Zeid (kabouzeid), Andrew Neal */ -public class MusicService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener { +public class MusicService extends Service implements SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks { public static final String TAG = MusicService.class.getSimpleName(); public static final String PHONOGRAPH_PACKAGE_NAME = "com.kabouzeid.gramophone"; @@ -100,7 +101,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP private static final int UNDUCK = 7; private final IBinder musicBind = new MusicBinder(); - private MultiPlayer player; + private Playback playback; private ArrayList playingQueue = new ArrayList<>(); private ArrayList originalPlayingQueue = new ArrayList<>(); private int position = -1; @@ -114,7 +115,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP @SuppressWarnings("deprecation") private RemoteControlClient remoteControlClient; private PowerManager.WakeLock wakeLock; - private MusicPlayerHandler playerHandler; + private PlaybackHandler playerHandler; private final AudioManager.OnAudioFocusChangeListener audioFocusListener = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(final int focusChange) { @@ -161,17 +162,15 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); wakeLock.setReferenceCounted(false); - musicPlayerHandlerThread = new HandlerThread("MusicPlayerHandler", - Process.THREAD_PRIORITY_BACKGROUND); + musicPlayerHandlerThread = new HandlerThread("PlaybackHandler"); musicPlayerHandlerThread.start(); - playerHandler = new MusicPlayerHandler(this, musicPlayerHandlerThread.getLooper()); + playerHandler = new PlaybackHandler(this, musicPlayerHandlerThread.getLooper()); - player = new MultiPlayer(this); - player.setHandler(playerHandler); + playback = new MultiPlayer(this); + playback.setCallbacks(this); - // queue saving needs to run on a separate thread so that it doesn't block the player handler events - queueSaveHandlerThread = new HandlerThread("QueueSaveHandler", - Process.THREAD_PRIORITY_BACKGROUND); + // queue saving needs to run on a separate thread so that it doesn't block the playback handler events + queueSaveHandlerThread = new HandlerThread("QueueSaveHandler", Process.THREAD_PRIORITY_BACKGROUND); queueSaveHandlerThread.start(); queueSaveHandler = new QueueSaveHandler(this, queueSaveHandlerThread.getLooper()); @@ -326,20 +325,20 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } else { queueSaveHandlerThread.quit(); } - player.release(); - player = null; + playback.release(); + playback = null; } public void stop() { pausedByTransientLossOfFocus = false; savePositionInTrack(); - player.stop(); + playback.stop(); notifyChange(PLAY_STATE_CHANGED); getAudioManager().abandonAudioFocus(audioFocusListener); } public boolean isPlaying() { - return player.isPlaying(); + return playback.isPlaying(); } public void saveState() { @@ -369,17 +368,13 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP return position; } - private void setPositionInternal(int position) { - this.position = position; - } - public void playNextSong(boolean force) { playSongAt(getNextPosition(force)); } private boolean openTrackAndPrepareNextAt(int position) { synchronized (this) { - setPositionInternal(position); + this.position = position; boolean prepared = openCurrent(); if (prepared) prepareNextImpl(); notifyChange(META_CHANGED); @@ -391,7 +386,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP private boolean openCurrent() { synchronized (this) { try { - return player.setDataSource(getTrackUri(getCurrentSong())); + return playback.setDataSource(getTrackUri(getCurrentSong())); } catch (Exception e) { return false; } @@ -407,7 +402,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP synchronized (this) { try { int nextPosition = getNextPosition(false); - player.setNextDataSource(getTrackUri(getSongAt(nextPosition))); + playback.setNextDataSource(getTrackUri(getSongAt(nextPosition))); this.nextPosition = nextPosition; return true; } catch (Exception e) { @@ -418,7 +413,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP private void closeAudioEffectSession() { final Intent audioEffectsIntent = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); - audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, player.getAudioSessionId()); + audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playback.getAudioSessionId()); audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName()); sendBroadcast(audioEffectsIntent); } @@ -606,7 +601,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP this.originalPlayingQueue = restoredOriginalQueue; this.playingQueue = restoredQueue; - setPositionInternal(restoredPosition); + position = restoredPosition; openCurrent(); prepareNext(); @@ -668,7 +663,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP private void rePosition(int deletedPosition) { int currentPosition = getPosition(); if (deletedPosition < currentPosition) { - setPositionInternal(currentPosition - 1); + position = currentPosition - 1; } else if (deletedPosition == currentPosition) { if (playingQueue.size() > deletedPosition) { setPosition(position); @@ -688,11 +683,11 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP originalPlayingQueue.add(to, tmpSong); } if (from > currentPosition && to <= currentPosition) { - setPositionInternal(currentPosition + 1); + position = currentPosition + 1; } else if (from < currentPosition && to >= currentPosition) { - setPositionInternal(currentPosition - 1); + position = currentPosition - 1; } else if (from == currentPosition) { - setPositionInternal(to); + position = to; } notifyChange(QUEUE_CHANGED); } @@ -727,8 +722,8 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP public void pause() { pausedByTransientLossOfFocus = false; - if (player.isPlaying()) { - player.pause(); + if (playback.isPlaying()) { + playback.pause(); notifyChange(PLAY_STATE_CHANGED); } } @@ -736,12 +731,12 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP public void play() { synchronized (this) { if (requestFocus()) { - if (!player.isPlaying()) { - if (!player.isInitialized()) { + if (!playback.isPlaying()) { + if (!playback.isInitialized()) { playSongAt(getPosition()); } else { registerReceiversAndRemoteControlClient(); - player.start(); + playback.start(); if (notHandledMetaChangedForCurrentTrack) { handleChange(META_CHANGED); notHandledMetaChangedForCurrentTrack = false; @@ -795,15 +790,15 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } public int getSongProgressMillis() { - return player.position(); + return playback.position(); } public int getSongDurationMillis() { - return player.duration(); + return playback.duration(); } public int seek(int millis) { - int newPosition = player.seek(millis); + int newPosition = playback.seek(millis); savePositionInTrack(); return newPosition; } @@ -842,7 +837,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP case SHUFFLE_MODE_SHUFFLE: this.shuffleMode = shuffleMode; ShuffleHelper.makeShuffleList(this.getPlayingQueue(), getPosition()); - setPositionInternal(0); + position = 0; break; case SHUFFLE_MODE_NONE: this.shuffleMode = shuffleMode; @@ -854,7 +849,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP newPosition = getPlayingQueue().indexOf(song); } } - setPositionInternal(newPosition); + position = newPosition; break; } notifyChange(SHUFFLE_MODE_CHANGED); @@ -924,7 +919,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } public int getAudioSessionId() { - return player.getAudioSessionId(); + return playback.getAudioSessionId(); } public void releaseWakeLock() { @@ -944,7 +939,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP if (sharedPreferences.getBoolean(key, false)) { prepareNext(); } else { - player.setNextDataSource(null); + playback.setNextDataSource(null); } break; case PreferenceUtil.ALBUM_ART_ON_LOCKSCREEN: @@ -957,6 +952,17 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } } + @Override + public void onTrackWentToNext() { + playerHandler.sendEmptyMessage(TRACK_WENT_TO_NEXT); + } + + @Override + public void onTrackEnded() { + acquireWakeLock(30000); + playerHandler.sendEmptyMessage(TRACK_ENDED); + } + private static final class QueueSaveHandler extends Handler { @NonNull private final WeakReference mService; @@ -977,12 +983,12 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } } - private static final class MusicPlayerHandler extends Handler { + private static final class PlaybackHandler extends Handler { @NonNull private final WeakReference mService; private float currentDuckVolume = 1.0f; - public MusicPlayerHandler(final MusicService service, @NonNull final Looper looper) { + public PlaybackHandler(final MusicService service, @NonNull final Looper looper) { super(looper); mService = new WeakReference<>(service); } @@ -1002,7 +1008,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } else { currentDuckVolume = .2f; } - service.player.setVolume(currentDuckVolume); + service.playback.setVolume(currentDuckVolume); break; case UNDUCK: @@ -1012,7 +1018,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } else { currentDuckVolume = 1.0f; } - service.player.setVolume(currentDuckVolume); + service.playback.setVolume(currentDuckVolume); break; case TRACK_WENT_TO_NEXT: @@ -1020,7 +1026,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP service.pause(); service.seek(0); } else { - service.setPositionInternal(service.nextPosition); + service.position = service.nextPosition; service.prepareNextImpl(); service.notifyChange(META_CHANGED); } @@ -1033,6 +1039,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP } else { service.playNextSong(false); } + sendEmptyMessage(RELEASE_WAKELOCK); break; case RELEASE_WAKELOCK: @@ -1065,14 +1072,14 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP break; case AudioManager.AUDIOFOCUS_LOSS: - // Lost focus for an unbounded amount of time: stop playback and release media player + // Lost focus for an unbounded amount of time: stop playback and release media playback service.pause(); service.unregisterReceiversAndRemoteControlClient(); break; case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: // Lost focus for a short time, but we have to stop - // playback. We don't release the media player because playback + // playback. We don't release the media playback because playback // is likely to resume boolean wasPlaying = service.isPlaying(); service.pause(); diff --git a/app/src/main/java/com/kabouzeid/gramophone/service/Playback/Playback.java b/app/src/main/java/com/kabouzeid/gramophone/service/Playback/Playback.java new file mode 100644 index 00000000..dc2783b4 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/gramophone/service/Playback/Playback.java @@ -0,0 +1,45 @@ +package com.kabouzeid.gramophone.service.Playback; + +import android.support.annotation.Nullable; + +/** + * @author Karim Abou Zeid (kabouzeid) + */ +public interface Playback { + + boolean setDataSource(String path); + + void setNextDataSource(@Nullable String path); + + void setCallbacks(PlaybackCallbacks callbacks); + + boolean isInitialized(); + + boolean start(); + + void stop(); + + void release(); + + boolean pause(); + + boolean isPlaying(); + + int duration(); + + int position(); + + int seek(int whereto); + + boolean setVolume(float vol); + + boolean setAudioSessionId(int sessionId); + + int getAudioSessionId(); + + interface PlaybackCallbacks { + void onTrackWentToNext(); + + void onTrackEnded(); + } +}