Handle playback with PlaybackManager
This commit is contained in:
parent
b58d971457
commit
70e323eb0a
6 changed files with 617 additions and 698 deletions
|
@ -14,7 +14,9 @@ import androidx.core.net.toUri
|
|||
import code.name.monkey.appthemehelper.util.VersionUtils.hasMarshmallow
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.extensions.showToast
|
||||
import code.name.monkey.retromusic.extensions.uri
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
import code.name.monkey.retromusic.service.AudioFader.Companion.createFadeAnimator
|
||||
import code.name.monkey.retromusic.service.playback.Playback
|
||||
import code.name.monkey.retromusic.service.playback.Playback.PlaybackCallbacks
|
||||
|
@ -124,18 +126,26 @@ class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletion
|
|||
override val isPlaying: Boolean
|
||||
get() = mIsInitialized && getCurrentPlayer()?.isPlaying == true
|
||||
|
||||
override fun setDataSource(path: String, force: Boolean): Boolean {
|
||||
override fun setDataSource(
|
||||
song: Song,
|
||||
force: Boolean,
|
||||
completion: (success: Boolean) -> Unit,
|
||||
) {
|
||||
cancelFade()
|
||||
if (force) hasDataSource = false
|
||||
mIsInitialized = false
|
||||
/* We've already set DataSource if initialized is true in setNextDataSource */
|
||||
if (!hasDataSource) {
|
||||
getCurrentPlayer()?.let { mIsInitialized = setDataSourceImpl(it, path) }
|
||||
getCurrentPlayer()?.let {
|
||||
setDataSourceImpl(it, song.uri.toString()) { success ->
|
||||
mIsInitialized = success
|
||||
completion(success)
|
||||
}
|
||||
}
|
||||
hasDataSource = true
|
||||
} else {
|
||||
mIsInitialized = true
|
||||
}
|
||||
return mIsInitialized
|
||||
}
|
||||
|
||||
override fun setNextDataSource(path: String?) {}
|
||||
|
@ -148,9 +158,9 @@ class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletion
|
|||
private fun setDataSourceImpl(
|
||||
player: MediaPlayer,
|
||||
path: String,
|
||||
): Boolean {
|
||||
completion: (success: Boolean) -> Unit,
|
||||
) {
|
||||
player.reset()
|
||||
player.setOnPreparedListener(null)
|
||||
try {
|
||||
if (path.startsWith("content://")) {
|
||||
player.setDataSource(context, path.toUri())
|
||||
|
@ -160,20 +170,18 @@ class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletion
|
|||
player.setAudioAttributes(
|
||||
AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build()
|
||||
)
|
||||
player.prepare()
|
||||
player.setPlaybackSpeedPitch(playbackSpeed, playbackPitch)
|
||||
player.setPlaybackSpeedPitch(PreferenceUtil.playbackSpeed, PreferenceUtil.playbackPitch)
|
||||
player.setOnPreparedListener {
|
||||
player.setOnPreparedListener(null)
|
||||
completion(true)
|
||||
}
|
||||
player.prepareAsync()
|
||||
} catch (e: Exception) {
|
||||
completion(false)
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
player.setOnCompletionListener(this)
|
||||
player.setOnErrorListener(this)
|
||||
val intent = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
||||
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
|
||||
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
||||
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
||||
context.sendBroadcast(intent)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setAudioSessionId(sessionId: Int): Boolean {
|
||||
|
@ -321,9 +329,11 @@ class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletion
|
|||
getNextPlayer()?.let { player ->
|
||||
val nextSong = MusicPlayerRemote.nextSong
|
||||
if (nextSong != null) {
|
||||
setDataSourceImpl(player, MusicUtil.getSongFileUri(nextSong.id).toString())
|
||||
// Switch to other player / Crossfade only if next song exists
|
||||
switchPlayer()
|
||||
setDataSourceImpl(player,
|
||||
MusicUtil.getSongFileUri(nextSong.id).toString()) { success ->
|
||||
// Switch to other player (Crossfade) only if next song exists
|
||||
if (success) switchPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,368 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.service;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.media.PlaybackParams;
|
||||
import android.media.audiofx.AudioEffect;
|
||||
import android.net.Uri;
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import code.name.monkey.appthemehelper.util.VersionUtils;
|
||||
import code.name.monkey.retromusic.R;
|
||||
import code.name.monkey.retromusic.service.playback.Playback;
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil;
|
||||
|
||||
/**
|
||||
* @author Andrew Neal, Karim Abou Zeid (kabouzeid)
|
||||
*/
|
||||
public class MultiPlayer
|
||||
implements Playback, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
|
||||
public static final String TAG = MultiPlayer.class.getSimpleName();
|
||||
|
||||
private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
|
||||
private MediaPlayer mNextMediaPlayer;
|
||||
|
||||
private final Context context;
|
||||
@Nullable
|
||||
private Playback.PlaybackCallbacks callbacks;
|
||||
|
||||
private boolean mIsInitialized = false;
|
||||
|
||||
/**
|
||||
* Constructor of <code>MultiPlayer</code>
|
||||
*/
|
||||
MultiPlayer(final Context context) {
|
||||
this.context = context;
|
||||
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path of the file, or the http/rtsp URL of the stream you want to play
|
||||
* @return True if the <code>player</code> has been prepared and is ready to play, false otherwise
|
||||
*/
|
||||
@Override
|
||||
public boolean setDataSource(@NotNull final String path, boolean force) {
|
||||
mIsInitialized = false;
|
||||
mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
|
||||
if (mIsInitialized) {
|
||||
setNextDataSource(null);
|
||||
}
|
||||
return mIsInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 <code>player</code> has been prepared and is ready to play, false otherwise
|
||||
*/
|
||||
private boolean setDataSourceImpl(@NonNull final MediaPlayer player, @NonNull final String path) {
|
||||
if (context == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
player.reset();
|
||||
player.setOnPreparedListener(null);
|
||||
if (path.startsWith("content://")) {
|
||||
player.setDataSource(context, Uri.parse(path));
|
||||
} else {
|
||||
player.setDataSource(path);
|
||||
}
|
||||
setPlaybackSpeedPitch(PreferenceUtil.INSTANCE.getPlaybackSpeed(), PreferenceUtil.INSTANCE.getPlaybackPitch());
|
||||
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
||||
player.prepare();
|
||||
} catch (Exception 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, context.getPackageName());
|
||||
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC);
|
||||
context.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
|
||||
*/
|
||||
@Override
|
||||
public void setNextDataSource(@Nullable final String path) {
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
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 (PreferenceUtil.INSTANCE.isGapLessPlayback()) {
|
||||
mNextMediaPlayer = new MediaPlayer();
|
||||
mNextMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
|
||||
mNextMediaPlayer.setAudioSessionId(getAudioSessionId());
|
||||
if (setDataSourceImpl(mNextMediaPlayer, path)) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
|
||||
} catch (@NonNull IllegalArgumentException | IllegalStateException e) {
|
||||
Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e);
|
||||
if (mNextMediaPlayer != null) {
|
||||
mNextMediaPlayer.release();
|
||||
mNextMediaPlayer = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mNextMediaPlayer != null) {
|
||||
mNextMediaPlayer.release();
|
||||
mNextMediaPlayer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callbacks
|
||||
*
|
||||
* @param callbacks The callbacks to use
|
||||
*/
|
||||
@Override
|
||||
public void setCallbacks(@Nullable final Playback.PlaybackCallbacks callbacks) {
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if the player is ready to go, false otherwise
|
||||
*/
|
||||
@Override
|
||||
public boolean isInitialized() {
|
||||
return mIsInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts or resumes playback.
|
||||
*/
|
||||
@Override
|
||||
public boolean start() {
|
||||
try {
|
||||
mCurrentMediaPlayer.start();
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the MediaPlayer to its uninitialized state.
|
||||
*/
|
||||
@Override
|
||||
public void stop() {
|
||||
mCurrentMediaPlayer.reset();
|
||||
mIsInitialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources associated with this MediaPlayer object.
|
||||
*/
|
||||
@Override
|
||||
public void release() {
|
||||
stop();
|
||||
mCurrentMediaPlayer.release();
|
||||
if (mNextMediaPlayer != null) {
|
||||
mNextMediaPlayer.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses playback. Call start() to resume.
|
||||
*/
|
||||
@Override
|
||||
public boolean pause() {
|
||||
try {
|
||||
mCurrentMediaPlayer.pause();
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the MultiPlayer is playing.
|
||||
*/
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return mIsInitialized && mCurrentMediaPlayer.isPlaying();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the duration of the file.
|
||||
*
|
||||
* @return The duration in milliseconds
|
||||
*/
|
||||
@Override
|
||||
public int duration() {
|
||||
if (!mIsInitialized) {
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
return mCurrentMediaPlayer.getDuration();
|
||||
} catch (IllegalStateException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current playback position.
|
||||
*
|
||||
* @return The current position in milliseconds
|
||||
*/
|
||||
@Override
|
||||
public int position() {
|
||||
if (!mIsInitialized) {
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
return mCurrentMediaPlayer.getCurrentPosition();
|
||||
} catch (IllegalStateException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@Override
|
||||
public int seek(final int whereto) {
|
||||
try {
|
||||
mCurrentMediaPlayer.seekTo(whereto);
|
||||
return whereto;
|
||||
} catch (IllegalStateException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVolume(final float vol) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setVolume(vol, vol);
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio session ID.
|
||||
*
|
||||
* @param sessionId The audio session ID
|
||||
*/
|
||||
@Override
|
||||
public boolean setAudioSessionId(final int sessionId) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setAudioSessionId(sessionId);
|
||||
return true;
|
||||
} catch (@NonNull IllegalArgumentException | IllegalStateException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the audio session ID.
|
||||
*
|
||||
* @return The current audio session ID.
|
||||
*/
|
||||
@Override
|
||||
public int getAudioSessionId() {
|
||||
return mCurrentMediaPlayer.getAudioSessionId();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean onError(final MediaPlayer mp, final int what, final int extra) {
|
||||
mIsInitialized = false;
|
||||
mCurrentMediaPlayer.release();
|
||||
mCurrentMediaPlayer = new MediaPlayer();
|
||||
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
|
||||
if (context != null) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getResources().getString(R.string.unplayable_file),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
Log.e(TAG, String.valueOf(what) + extra);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onCompletion(final MediaPlayer mp) {
|
||||
if (mp.equals(mCurrentMediaPlayer) && mNextMediaPlayer != null) {
|
||||
mIsInitialized = false;
|
||||
mCurrentMediaPlayer.release();
|
||||
mCurrentMediaPlayer = mNextMediaPlayer;
|
||||
mIsInitialized = true;
|
||||
mNextMediaPlayer = null;
|
||||
if (callbacks != null) callbacks.onTrackWentToNext();
|
||||
} else {
|
||||
if (callbacks != null) callbacks.onTrackEnded();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCrossFadeDuration(int duration) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackSpeedPitch(float speed, float pitch) {
|
||||
if (VersionUtils.INSTANCE.hasMarshmallow()) {
|
||||
boolean wasPlaying = mCurrentMediaPlayer.isPlaying();
|
||||
mCurrentMediaPlayer.setPlaybackParams(new PlaybackParams()
|
||||
.setSpeed(PreferenceUtil.INSTANCE.getPlaybackSpeed())
|
||||
.setPitch(PreferenceUtil.INSTANCE.getPlaybackPitch()));
|
||||
if (!wasPlaying) {
|
||||
if (mCurrentMediaPlayer.isPlaying()) mCurrentMediaPlayer.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,379 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
package code.name.monkey.retromusic.service
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import android.media.MediaPlayer.OnCompletionListener
|
||||
import android.media.PlaybackParams
|
||||
import android.media.audiofx.AudioEffect
|
||||
import android.net.Uri
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceManager
|
||||
import code.name.monkey.appthemehelper.util.VersionUtils.hasMarshmallow
|
||||
import code.name.monkey.retromusic.PLAYBACK_PITCH
|
||||
import code.name.monkey.retromusic.PLAYBACK_SPEED
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.extensions.showToast
|
||||
import code.name.monkey.retromusic.extensions.uri
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
import code.name.monkey.retromusic.service.playback.Playback
|
||||
import code.name.monkey.retromusic.service.playback.Playback.PlaybackCallbacks
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil.isGapLessPlayback
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil.playbackPitch
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil.playbackSpeed
|
||||
|
||||
/**
|
||||
* @author Andrew Neal, Karim Abou Zeid (kabouzeid)
|
||||
*/
|
||||
class MultiPlayer internal constructor(private val context: Context) : Playback,
|
||||
MediaPlayer.OnErrorListener, OnCompletionListener, OnSharedPreferenceChangeListener {
|
||||
private var mCurrentMediaPlayer = MediaPlayer()
|
||||
private var mNextMediaPlayer: MediaPlayer? = null
|
||||
private var callbacks: PlaybackCallbacks? = null
|
||||
|
||||
/**
|
||||
* @return True if the player is ready to go, false otherwise
|
||||
*/
|
||||
override var isInitialized = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
override fun setDataSource(
|
||||
song: Song,
|
||||
force: Boolean,
|
||||
completion: (success: Boolean) -> Unit,
|
||||
) {
|
||||
isInitialized = false
|
||||
setDataSourceImpl(mCurrentMediaPlayer, song.uri.toString()) { success ->
|
||||
isInitialized = success
|
||||
if (isInitialized) {
|
||||
setNextDataSource(null)
|
||||
}
|
||||
completion(isInitialized)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param player The [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 fun setDataSourceImpl(
|
||||
player: MediaPlayer,
|
||||
path: String,
|
||||
completion: (success: Boolean) -> Unit,
|
||||
) {
|
||||
try {
|
||||
player.reset()
|
||||
player.setOnPreparedListener(null)
|
||||
if (path.startsWith("content://")) {
|
||||
player.setDataSource(context, Uri.parse(path))
|
||||
} else {
|
||||
player.setDataSource(path)
|
||||
}
|
||||
setPlaybackSpeedPitch(player)
|
||||
player.setAudioAttributes(AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
)
|
||||
player.setOnPreparedListener {
|
||||
player.setOnPreparedListener(null)
|
||||
completion(true)
|
||||
}
|
||||
player.prepareAsync()
|
||||
} catch (e: Exception) {
|
||||
completion(false)
|
||||
e.printStackTrace()
|
||||
}
|
||||
player.setOnCompletionListener(this)
|
||||
player.setOnErrorListener(this)
|
||||
}
|
||||
|
||||
fun openEqualizerSession() {
|
||||
val intent = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
||||
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
|
||||
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
||||
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
fun closeEqualizerSession() {
|
||||
val intent = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
||||
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
|
||||
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
||||
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
override fun setNextDataSource(path: String?) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setNextMediaPlayer(null)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.i(TAG, "Next media player is current one, continuing")
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, "Media player not initialized!")
|
||||
return
|
||||
}
|
||||
if (mNextMediaPlayer != null) {
|
||||
mNextMediaPlayer?.release()
|
||||
mNextMediaPlayer = null
|
||||
}
|
||||
if (path == null) {
|
||||
return
|
||||
}
|
||||
if (isGapLessPlayback) {
|
||||
mNextMediaPlayer = MediaPlayer()
|
||||
mNextMediaPlayer?.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
|
||||
mNextMediaPlayer?.audioSessionId = audioSessionId
|
||||
setDataSourceImpl(mNextMediaPlayer!!, path) { success ->
|
||||
if (success) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e)
|
||||
if (mNextMediaPlayer != null) {
|
||||
mNextMediaPlayer?.release()
|
||||
mNextMediaPlayer = null
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e)
|
||||
if (mNextMediaPlayer != null) {
|
||||
mNextMediaPlayer?.release()
|
||||
mNextMediaPlayer = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (mNextMediaPlayer != null) {
|
||||
mNextMediaPlayer?.release()
|
||||
mNextMediaPlayer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callbacks
|
||||
*
|
||||
* @param callbacks The callbacks to use
|
||||
*/
|
||||
override fun setCallbacks(callbacks: PlaybackCallbacks) {
|
||||
this.callbacks = callbacks
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts or resumes playback.
|
||||
*/
|
||||
override fun start(): Boolean {
|
||||
return try {
|
||||
mCurrentMediaPlayer.start()
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the MediaPlayer to its uninitialized state.
|
||||
*/
|
||||
override fun stop() {
|
||||
mCurrentMediaPlayer.reset()
|
||||
isInitialized = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources associated with this MediaPlayer object.
|
||||
*/
|
||||
override fun release() {
|
||||
stop()
|
||||
mCurrentMediaPlayer.release()
|
||||
if (mNextMediaPlayer != null) {
|
||||
mNextMediaPlayer?.release()
|
||||
}
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses playback. Call start() to resume.
|
||||
*/
|
||||
override fun pause(): Boolean {
|
||||
return try {
|
||||
mCurrentMediaPlayer.pause()
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the MultiPlayer is playing.
|
||||
*/
|
||||
override val isPlaying: Boolean
|
||||
get() = isInitialized && mCurrentMediaPlayer.isPlaying
|
||||
|
||||
/**
|
||||
* Gets the duration of the file.
|
||||
*
|
||||
* @return The duration in milliseconds
|
||||
*/
|
||||
override fun duration(): Int {
|
||||
return if (!this.isInitialized) {
|
||||
-1
|
||||
} else try {
|
||||
mCurrentMediaPlayer.duration
|
||||
} catch (e: IllegalStateException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current playback position.
|
||||
*
|
||||
* @return The current position in milliseconds
|
||||
*/
|
||||
override fun position(): Int {
|
||||
return if (!this.isInitialized) {
|
||||
-1
|
||||
} else try {
|
||||
mCurrentMediaPlayer.currentPosition
|
||||
} catch (e: IllegalStateException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
override fun seek(whereto: Int): Int {
|
||||
return try {
|
||||
mCurrentMediaPlayer.seekTo(whereto)
|
||||
whereto
|
||||
} catch (e: IllegalStateException) {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
override fun setVolume(vol: Float): Boolean {
|
||||
return try {
|
||||
mCurrentMediaPlayer.setVolume(vol, vol)
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio session ID.
|
||||
*
|
||||
* @param sessionId The audio session ID
|
||||
*/
|
||||
override fun setAudioSessionId(sessionId: Int): Boolean {
|
||||
return try {
|
||||
mCurrentMediaPlayer.audioSessionId = sessionId
|
||||
true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
false
|
||||
} catch (e: IllegalStateException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the audio session ID.
|
||||
*
|
||||
* @return The current audio session ID.
|
||||
*/
|
||||
override val audioSessionId: Int
|
||||
get() = mCurrentMediaPlayer.audioSessionId
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||
isInitialized = false
|
||||
mCurrentMediaPlayer.release()
|
||||
mCurrentMediaPlayer = MediaPlayer()
|
||||
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
|
||||
context.showToast(R.string.unplayable_file)
|
||||
Log.e(TAG, what.toString() + extra)
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
override fun onCompletion(mp: MediaPlayer) {
|
||||
if (mp == mCurrentMediaPlayer && mNextMediaPlayer != null) {
|
||||
isInitialized = false
|
||||
mCurrentMediaPlayer.release()
|
||||
mCurrentMediaPlayer = mNextMediaPlayer!!
|
||||
isInitialized = true
|
||||
mNextMediaPlayer = null
|
||||
if (callbacks != null) callbacks?.onTrackWentToNext()
|
||||
} else {
|
||||
if (callbacks != null) callbacks?.onTrackEnded()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setCrossFadeDuration(duration: Int) {}
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
if (key == PLAYBACK_SPEED || key == PLAYBACK_PITCH) {
|
||||
setPlaybackSpeedPitch(mCurrentMediaPlayer)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPlaybackSpeedPitch(mp: MediaPlayer) {
|
||||
if (hasMarshmallow()) {
|
||||
val wasPlaying = mp.isPlaying
|
||||
mp.playbackParams = PlaybackParams()
|
||||
.setSpeed(playbackSpeed)
|
||||
.setPitch(playbackPitch)
|
||||
if (!wasPlaying) {
|
||||
if (mp.isPlaying) mp.pause()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG: String = MultiPlayer::class.java.simpleName
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor of `MultiPlayer`
|
||||
*/
|
||||
init {
|
||||
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
}
|
|
@ -25,7 +25,6 @@ import android.graphics.Bitmap
|
|||
import android.graphics.drawable.Drawable
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||
import android.media.audiofx.AudioEffect
|
||||
import android.os.*
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
|
@ -57,11 +56,11 @@ import code.name.monkey.retromusic.auto.AutoMediaIDHelper
|
|||
import code.name.monkey.retromusic.auto.AutoMusicProvider
|
||||
import code.name.monkey.retromusic.extensions.showToast
|
||||
import code.name.monkey.retromusic.extensions.toMediaSessionQueue
|
||||
import code.name.monkey.retromusic.extensions.uri
|
||||
import code.name.monkey.retromusic.glide.BlurTransformation
|
||||
import code.name.monkey.retromusic.glide.GlideApp
|
||||
import code.name.monkey.retromusic.glide.RetroGlideExtension.getDefaultTransition
|
||||
import code.name.monkey.retromusic.glide.RetroGlideExtension.getSongModel
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote.isCasting
|
||||
import code.name.monkey.retromusic.helper.ShuffleHelper.makeShuffleList
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
import code.name.monkey.retromusic.model.Song.Companion.emptySong
|
||||
|
@ -69,14 +68,12 @@ import code.name.monkey.retromusic.model.smartplaylist.AbsSmartPlaylist
|
|||
import code.name.monkey.retromusic.providers.HistoryStore
|
||||
import code.name.monkey.retromusic.providers.MusicPlaybackQueueStore
|
||||
import code.name.monkey.retromusic.providers.SongPlayCountStore
|
||||
import code.name.monkey.retromusic.service.AudioFader.Companion.startFadeAnimator
|
||||
import code.name.monkey.retromusic.service.notification.PlayingNotification
|
||||
import code.name.monkey.retromusic.service.notification.PlayingNotificationClassic
|
||||
import code.name.monkey.retromusic.service.notification.PlayingNotificationImpl24
|
||||
import code.name.monkey.retromusic.service.playback.Playback
|
||||
import code.name.monkey.retromusic.service.playback.Playback.PlaybackCallbacks
|
||||
import code.name.monkey.retromusic.util.MusicUtil.getMediaStoreAlbumCoverUri
|
||||
import code.name.monkey.retromusic.util.MusicUtil.getSongFileUri
|
||||
import code.name.monkey.retromusic.util.MusicUtil.toggleFavorite
|
||||
import code.name.monkey.retromusic.util.PackageValidator
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil.crossFadeDuration
|
||||
|
@ -96,6 +93,7 @@ import code.name.monkey.retromusic.volume.OnAudioVolumeChangedListener
|
|||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.google.android.gms.cast.framework.CastSession
|
||||
import org.koin.java.KoinJavaComponent.get
|
||||
import java.util.*
|
||||
|
||||
|
@ -112,8 +110,10 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
@JvmField
|
||||
var pendingQuit = false
|
||||
|
||||
@JvmField
|
||||
var playback: Playback? = null
|
||||
private lateinit var playbackManager: PlaybackManager
|
||||
|
||||
val playback: Playback? get() = playbackManager.playback
|
||||
|
||||
private var mPackageValidator: PackageValidator? = null
|
||||
private val mMusicProvider = get<AutoMusicProvider>(AutoMusicProvider::class.java)
|
||||
private var trackEndedByCrossfade = false
|
||||
|
@ -288,15 +288,10 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
wakeLock?.setReferenceCounted(false)
|
||||
musicPlayerHandlerThread = HandlerThread("PlaybackHandler")
|
||||
musicPlayerHandlerThread?.start()
|
||||
playerHandler = PlaybackHandler(this, musicPlayerHandlerThread!!.looper)
|
||||
playerHandler = PlaybackHandler(this, mainLooper)
|
||||
|
||||
// Set MultiPlayer when crossfade duration is 0 i.e. off
|
||||
playback = if (crossFadeDuration == 0) {
|
||||
MultiPlayer(this)
|
||||
} else {
|
||||
CrossFadePlayer(this)
|
||||
}
|
||||
playback?.setCallbacks(this)
|
||||
playbackManager = PlaybackManager(this)
|
||||
playbackManager.setCallbacks(this)
|
||||
setupMediaSession()
|
||||
|
||||
// queue saving needs to run on a separate thread so that it doesn't block the playback handler
|
||||
|
@ -457,11 +452,11 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
}
|
||||
|
||||
val audioSessionId: Int
|
||||
get() = if (playback != null) {
|
||||
playback!!.audioSessionId
|
||||
} else -1
|
||||
get() = playbackManager.audioSessionId
|
||||
|
||||
val currentSong: Song
|
||||
get() = getSongAt(getPosition())
|
||||
|
||||
val nextSong: Song?
|
||||
get() = if (isLastTrack && repeatMode == REPEAT_MODE_NONE) {
|
||||
null
|
||||
|
@ -501,9 +496,11 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
}
|
||||
|
||||
private fun setPosition(position: Int) {
|
||||
// handle this on the handlers thread to avoid blocking the ui thread
|
||||
playerHandler?.removeMessages(SET_POSITION)
|
||||
playerHandler?.obtainMessage(SET_POSITION, position, 0)?.sendToTarget()
|
||||
openTrackAndPrepareNextAt(position) { success ->
|
||||
if (success) {
|
||||
notifyChange(PLAY_STATE_CHANGED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPreviousPosition(force: Boolean): Int {
|
||||
|
@ -577,13 +574,10 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
}
|
||||
|
||||
val songDurationMillis: Int
|
||||
get() = if (playback != null) {
|
||||
playback!!.duration()
|
||||
} else -1
|
||||
get() = playbackManager.songDurationMillis
|
||||
|
||||
val songProgressMillis: Int
|
||||
get() = if (playback != null) {
|
||||
playback!!.position()
|
||||
} else -1
|
||||
get() = playbackManager.songProgressMillis
|
||||
|
||||
fun handleAndSendChangeInternal(what: String) {
|
||||
handleChangeInternal(what)
|
||||
|
@ -602,8 +596,9 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
|
||||
val isLastTrack: Boolean
|
||||
get() = getPosition() == playingQueue.size - 1
|
||||
|
||||
val isPlaying: Boolean
|
||||
get() = playback != null && playback!!.isPlaying
|
||||
get() = playbackManager.isPlaying
|
||||
|
||||
fun moveSong(from: Int, to: Int) {
|
||||
if (from == to) {
|
||||
|
@ -702,38 +697,10 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
CROSS_FADE_DURATION -> {
|
||||
val progress = songProgressMillis
|
||||
val wasPlaying = isPlaying
|
||||
/* Switch to MultiPlayer if Crossfade duration is 0 and
|
||||
Playback is not an instance of MultiPlayer */
|
||||
if (playback !is MultiPlayer && crossFadeDuration == 0) {
|
||||
if (playback != null) {
|
||||
playback?.release()
|
||||
}
|
||||
playback = null
|
||||
playback = MultiPlayer(this)
|
||||
playback?.setCallbacks(this)
|
||||
if (openTrackAndPrepareNextAt(position)) {
|
||||
seek(progress)
|
||||
if (wasPlaying) {
|
||||
play()
|
||||
}
|
||||
}
|
||||
} else if (playback !is CrossFadePlayer && crossFadeDuration > 0) {
|
||||
if (playback != null) {
|
||||
playback?.release()
|
||||
}
|
||||
playback = null
|
||||
playback = CrossFadePlayer(this)
|
||||
playback?.setCallbacks(this)
|
||||
if (openTrackAndPrepareNextAt(position)) {
|
||||
seek(progress)
|
||||
if (wasPlaying) {
|
||||
play()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (playback != null) playback?.setCrossFadeDuration(
|
||||
crossFadeDuration
|
||||
)
|
||||
|
||||
playbackManager.maybeSwitchToCrossFade(crossFadeDuration)
|
||||
restorePlaybackState(wasPlaying, progress)
|
||||
|
||||
}
|
||||
ALBUM_ART_ON_LOCK_SCREEN, BLURRED_ALBUM_ART -> updateMediaSessionMetaData()
|
||||
COLORED_NOTIFICATION -> {
|
||||
|
@ -795,6 +762,10 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
playerHandler?.sendEmptyMessage(TRACK_WENT_TO_NEXT)
|
||||
}
|
||||
|
||||
override fun onPlayStateChanged() {
|
||||
notifyChange(PLAY_STATE_CHANGED)
|
||||
}
|
||||
|
||||
override fun onUnbind(intent: Intent): Boolean {
|
||||
if (!isPlaying) {
|
||||
stopSelf()
|
||||
|
@ -828,34 +799,21 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
fun openTrackAndPrepareNextAt(position: Int): Boolean {
|
||||
fun openTrackAndPrepareNextAt(position: Int, completion: (success: Boolean) -> Unit) {
|
||||
this.position = position
|
||||
val prepared = openCurrent()
|
||||
if (prepared) {
|
||||
openCurrent { success ->
|
||||
if (success) {
|
||||
prepareNextImpl()
|
||||
}
|
||||
notifyChange(META_CHANGED)
|
||||
notHandledMetaChangedForCurrentTrack = false
|
||||
return prepared
|
||||
completion(success)
|
||||
}
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
Log.i(TAG, "Paused")
|
||||
fun pause(force: Boolean = false) {
|
||||
isPausedByTransientLossOfFocus = false
|
||||
if (playback != null && playback!!.isPlaying) {
|
||||
startFadeAnimator(playback!!, false) {
|
||||
|
||||
//Code to run when Animator Ends
|
||||
playback?.pause()
|
||||
notifyChange(PLAY_STATE_CHANGED)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun forcePause() {
|
||||
isPausedByTransientLossOfFocus = false
|
||||
if (playback != null && playback!!.isPlaying) {
|
||||
playback?.pause()
|
||||
playbackManager.pause(force) {
|
||||
notifyChange(PLAY_STATE_CHANGED)
|
||||
}
|
||||
}
|
||||
|
@ -863,17 +821,8 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
@Synchronized
|
||||
fun play() {
|
||||
if (requestFocus()) {
|
||||
if (playback != null && !playback!!.isPlaying) {
|
||||
if (!playback!!.isInitialized) {
|
||||
playSongAt(getPosition())
|
||||
} else {
|
||||
//Don't Start playing when it's casting
|
||||
if (isCasting) {
|
||||
return
|
||||
}
|
||||
startFadeAnimator(playback!!, true) {
|
||||
playbackManager.play(onNotInitialized = { playSongAt(getPosition()) }) {
|
||||
|
||||
// Code when Animator Ends
|
||||
if (!becomingNoisyReceiverRegistered) {
|
||||
registerReceiver(
|
||||
becomingNoisyReceiver,
|
||||
|
@ -891,11 +840,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
playerHandler?.removeMessages(DUCK)
|
||||
playerHandler?.sendEmptyMessage(UNDUCK)
|
||||
}
|
||||
//Start Playback with Animator
|
||||
playback?.start()
|
||||
notifyChange(PLAY_STATE_CHANGED)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showToast(R.string.audio_focus_denied)
|
||||
}
|
||||
|
@ -910,18 +855,14 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
}
|
||||
|
||||
fun playSongAt(position: Int) {
|
||||
// handle this on the handlers thread to avoid blocking the ui thread
|
||||
playerHandler?.removeMessages(PLAY_SONG)
|
||||
playerHandler?.obtainMessage(PLAY_SONG, position, 0)?.sendToTarget()
|
||||
}
|
||||
|
||||
fun playSongAtImpl(position: Int) {
|
||||
if (openTrackAndPrepareNextAt(position)) {
|
||||
openTrackAndPrepareNextAt(position) { success ->
|
||||
if (success) {
|
||||
play()
|
||||
} else {
|
||||
showToast(resources.getString(R.string.unplayable_file))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun playSongs(songs: ArrayList<Song>?, shuffleMode: Int) {
|
||||
if (songs != null && songs.isNotEmpty()) {
|
||||
|
@ -942,7 +883,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
fun prepareNextImpl() {
|
||||
try {
|
||||
val nextPosition = getNextPosition(false)
|
||||
playback?.setNextDataSource(getTrackUri(getSongAt(nextPosition)))
|
||||
playbackManager.setNextDataSource(getSongAt(nextPosition).uri.toString())
|
||||
this.nextPosition = nextPosition
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
|
@ -952,7 +893,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
pause()
|
||||
stopForeground(true)
|
||||
notificationManager?.cancel(PlayingNotification.NOTIFICATION_ID)
|
||||
closeAudioEffectSession()
|
||||
playbackManager.closeAudioEffectSession()
|
||||
audioManager?.abandonAudioFocus(audioFocusListener)
|
||||
stopSelf()
|
||||
}
|
||||
|
@ -1030,13 +971,15 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
originalPlayingQueue = ArrayList(restoredOriginalQueue)
|
||||
playingQueue = ArrayList(restoredQueue)
|
||||
position = restoredPosition
|
||||
openCurrent()
|
||||
openCurrent {
|
||||
prepareNext()
|
||||
if (restoredPositionInTrack > 0) {
|
||||
seek(restoredPositionInTrack)
|
||||
}
|
||||
notHandledMetaChangedForCurrentTrack = true
|
||||
sendChangeInternal(META_CHANGED)
|
||||
}
|
||||
|
||||
sendChangeInternal(QUEUE_CHANGED)
|
||||
}
|
||||
}
|
||||
|
@ -1067,9 +1010,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
fun seek(millis: Int): Int {
|
||||
return try {
|
||||
var newPosition = 0
|
||||
if (playback != null) {
|
||||
newPosition = playback!!.seek(millis)
|
||||
}
|
||||
newPosition = playbackManager.seek(millis)
|
||||
throttledSeekHandler?.notifySeek()
|
||||
newPosition
|
||||
} catch (e: Exception) {
|
||||
|
@ -1178,15 +1119,6 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
}
|
||||
}
|
||||
|
||||
private fun closeAudioEffectSession() {
|
||||
val audioEffectsIntent = Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
|
||||
if (playback != null) {
|
||||
audioEffectsIntent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, playback!!.audioSessionId)
|
||||
}
|
||||
audioEffectsIntent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
|
||||
sendBroadcast(audioEffectsIntent)
|
||||
}
|
||||
|
||||
private fun handleChangeInternal(what: String) {
|
||||
when (what) {
|
||||
PLAY_STATE_CHANGED -> {
|
||||
|
@ -1284,23 +1216,39 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
private fun openCurrent(): Boolean {
|
||||
private fun openCurrent(completion: (success: Boolean) -> Unit) {
|
||||
val force = if (!trackEndedByCrossfade) {
|
||||
true
|
||||
} else {
|
||||
trackEndedByCrossfade = false
|
||||
false
|
||||
}
|
||||
return try {
|
||||
if (playback != null) {
|
||||
return playback!!.setDataSource(getTrackUri(currentSong), force)
|
||||
} else false
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
playbackManager.setDataSource(currentSong, force) { success ->
|
||||
completion(success)
|
||||
}
|
||||
}
|
||||
|
||||
fun switchToLocalPlayback() {
|
||||
playbackManager.switchToLocalPlayback(this::restorePlaybackState)
|
||||
}
|
||||
|
||||
fun switchToRemotePlayback(castSession: CastSession) {
|
||||
playbackManager.switchToRemotePlayback(castSession, this::restorePlaybackState)
|
||||
}
|
||||
|
||||
private fun restorePlaybackState(wasPlaying: Boolean, progress: Int) {
|
||||
playbackManager.setCallbacks(this)
|
||||
openTrackAndPrepareNextAt(position) { success ->
|
||||
if (success) {
|
||||
seek(progress)
|
||||
if (wasPlaying) {
|
||||
play()
|
||||
}
|
||||
}
|
||||
}
|
||||
playbackManager.setCrossFadeDuration(crossFadeDuration)
|
||||
}
|
||||
|
||||
private fun playFromPlaylist(intent: Intent) {
|
||||
val playlist: AbsSmartPlaylist? = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST)
|
||||
val shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode())
|
||||
|
@ -1347,10 +1295,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
musicPlayerHandlerThread?.quitSafely()
|
||||
queueSaveHandler?.removeCallbacksAndMessages(null)
|
||||
queueSaveHandlerThread?.quitSafely()
|
||||
if (playback != null) {
|
||||
playback?.release()
|
||||
}
|
||||
playback = null
|
||||
playbackManager.release()
|
||||
mediaSession?.release()
|
||||
}
|
||||
|
||||
|
@ -1508,9 +1453,5 @@ class MusicService : MediaBrowserServiceCompat(),
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTrackUri(song: Song): String {
|
||||
return getSongFileUri(song.id).toString()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,165 +11,116 @@
|
|||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
package code.name.monkey.retromusic.service
|
||||
|
||||
package code.name.monkey.retromusic.service;
|
||||
import android.media.AudioManager
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Message
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil.isAudioDucking
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil.isAudioFocusEnabled
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
import static code.name.monkey.retromusic.service.MusicService.DUCK;
|
||||
import static code.name.monkey.retromusic.service.MusicService.META_CHANGED;
|
||||
import static code.name.monkey.retromusic.service.MusicService.PLAY_STATE_CHANGED;
|
||||
import static code.name.monkey.retromusic.service.MusicService.REPEAT_MODE_NONE;
|
||||
import static code.name.monkey.retromusic.service.MusicService.TRACK_ENDED;
|
||||
import static code.name.monkey.retromusic.service.MusicService.TRACK_WENT_TO_NEXT;
|
||||
internal class PlaybackHandler(service: MusicService, looper: Looper) : Handler(looper) {
|
||||
private val mService: WeakReference<MusicService>
|
||||
private var currentDuckVolume = 1.0f
|
||||
|
||||
import android.media.AudioManager;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil;
|
||||
|
||||
class PlaybackHandler extends Handler {
|
||||
|
||||
@NonNull private final WeakReference<MusicService> mService;
|
||||
private float currentDuckVolume = 1.0f;
|
||||
|
||||
|
||||
PlaybackHandler(final MusicService service, @NonNull final Looper looper) {
|
||||
super(looper);
|
||||
mService = new WeakReference<>(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(@NonNull final Message msg) {
|
||||
final MusicService service = mService.get();
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (msg.what) {
|
||||
case MusicService.DUCK:
|
||||
if (PreferenceUtil.INSTANCE.isAudioDucking()) {
|
||||
currentDuckVolume -= .05f;
|
||||
override fun handleMessage(msg: Message) {
|
||||
val service = mService.get() ?: return
|
||||
when (msg.what) {
|
||||
MusicService.DUCK -> {
|
||||
if (isAudioDucking) {
|
||||
currentDuckVolume -= .05f
|
||||
if (currentDuckVolume > .2f) {
|
||||
sendEmptyMessageDelayed(DUCK, 10);
|
||||
sendEmptyMessageDelayed(MusicService.DUCK, 10)
|
||||
} else {
|
||||
currentDuckVolume = .2f;
|
||||
currentDuckVolume = .2f
|
||||
}
|
||||
} else {
|
||||
currentDuckVolume = 1f;
|
||||
currentDuckVolume = 1f
|
||||
}
|
||||
service.playback.setVolume(currentDuckVolume);
|
||||
break;
|
||||
|
||||
case MusicService.UNDUCK:
|
||||
if (PreferenceUtil.INSTANCE.isAudioDucking()) {
|
||||
currentDuckVolume += .03f;
|
||||
service.playback?.setVolume(currentDuckVolume)
|
||||
}
|
||||
MusicService.UNDUCK -> {
|
||||
if (isAudioDucking) {
|
||||
currentDuckVolume += .03f
|
||||
if (currentDuckVolume < 1f) {
|
||||
sendEmptyMessageDelayed(MusicService.UNDUCK, 10);
|
||||
sendEmptyMessageDelayed(MusicService.UNDUCK, 10)
|
||||
} else {
|
||||
currentDuckVolume = 1f;
|
||||
currentDuckVolume = 1f
|
||||
}
|
||||
} else {
|
||||
currentDuckVolume = 1f;
|
||||
currentDuckVolume = 1f
|
||||
}
|
||||
service.playback.setVolume(currentDuckVolume);
|
||||
break;
|
||||
|
||||
case TRACK_WENT_TO_NEXT:
|
||||
if (service.pendingQuit
|
||||
|| service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) {
|
||||
service.pause();
|
||||
service.seek(0);
|
||||
service.playback?.setVolume(currentDuckVolume)
|
||||
}
|
||||
MusicService.TRACK_WENT_TO_NEXT ->
|
||||
if (service.pendingQuit || service.repeatMode == MusicService.REPEAT_MODE_NONE && service.isLastTrack) {
|
||||
service.pause(false)
|
||||
service.seek(0)
|
||||
if (service.pendingQuit) {
|
||||
service.pendingQuit = false;
|
||||
service.quit();
|
||||
break;
|
||||
service.pendingQuit = false
|
||||
service.quit()
|
||||
}
|
||||
} else {
|
||||
service.position = service.nextPosition;
|
||||
service.prepareNextImpl();
|
||||
service.notifyChange(META_CHANGED);
|
||||
service.position = service.nextPosition
|
||||
service.prepareNextImpl()
|
||||
service.notifyChange(MusicService.META_CHANGED)
|
||||
}
|
||||
break;
|
||||
|
||||
case TRACK_ENDED:
|
||||
MusicService.TRACK_ENDED -> {
|
||||
// if there is a timer finished, don't continue
|
||||
if (service.pendingQuit
|
||||
|| service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) {
|
||||
service.notifyChange(PLAY_STATE_CHANGED);
|
||||
service.seek(0);
|
||||
|| service.repeatMode == MusicService.REPEAT_MODE_NONE && service.isLastTrack
|
||||
) {
|
||||
service.notifyChange(MusicService.PLAY_STATE_CHANGED)
|
||||
service.seek(0)
|
||||
if (service.pendingQuit) {
|
||||
service.pendingQuit = false;
|
||||
service.quit();
|
||||
break;
|
||||
service.pendingQuit = false
|
||||
service.quit()
|
||||
}
|
||||
} else {
|
||||
service.playNextSong(false);
|
||||
service.playNextSong(false)
|
||||
}
|
||||
sendEmptyMessage(MusicService.RELEASE_WAKELOCK);
|
||||
break;
|
||||
|
||||
case MusicService.RELEASE_WAKELOCK:
|
||||
service.releaseWakeLock();
|
||||
break;
|
||||
|
||||
case MusicService.PLAY_SONG:
|
||||
service.playSongAtImpl(msg.arg1);
|
||||
break;
|
||||
|
||||
case MusicService.SET_POSITION:
|
||||
service.openTrackAndPrepareNextAt(msg.arg1);
|
||||
service.notifyChange(PLAY_STATE_CHANGED);
|
||||
break;
|
||||
|
||||
case MusicService.PREPARE_NEXT:
|
||||
service.prepareNextImpl();
|
||||
break;
|
||||
|
||||
case MusicService.RESTORE_QUEUES:
|
||||
service.restoreQueuesAndPositionIfNecessary();
|
||||
break;
|
||||
|
||||
case MusicService.FOCUS_CHANGE:
|
||||
switch (msg.arg1) {
|
||||
case AudioManager.AUDIOFOCUS_GAIN:
|
||||
if (!service.isPlaying() && service.isPausedByTransientLossOfFocus()) {
|
||||
service.play();
|
||||
service.setPausedByTransientLossOfFocus(false);
|
||||
sendEmptyMessage(MusicService.RELEASE_WAKELOCK)
|
||||
}
|
||||
removeMessages(DUCK);
|
||||
sendEmptyMessage(MusicService.UNDUCK);
|
||||
break;
|
||||
|
||||
case AudioManager.AUDIOFOCUS_LOSS:
|
||||
MusicService.RELEASE_WAKELOCK -> service.releaseWakeLock()
|
||||
MusicService.PREPARE_NEXT -> service.prepareNextImpl()
|
||||
MusicService.RESTORE_QUEUES -> service.restoreQueuesAndPositionIfNecessary()
|
||||
MusicService.FOCUS_CHANGE -> when (msg.arg1) {
|
||||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
if (!service.isPlaying && service.isPausedByTransientLossOfFocus) {
|
||||
service.play()
|
||||
service.isPausedByTransientLossOfFocus = false
|
||||
}
|
||||
removeMessages(MusicService.DUCK)
|
||||
sendEmptyMessage(MusicService.UNDUCK)
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
// Lost focus for an unbounded amount of time: stop playback and release media playback
|
||||
boolean isAudioFocusEnabled = PreferenceUtil.INSTANCE.isAudioFocusEnabled();
|
||||
val isAudioFocusEnabled = isAudioFocusEnabled
|
||||
if (!isAudioFocusEnabled) {
|
||||
service.forcePause();
|
||||
service.pause(true)
|
||||
}
|
||||
break;
|
||||
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
// Lost focus for a short time, but we have to stop
|
||||
// playback. We don't release the media playback because playback
|
||||
// is likely to resume
|
||||
boolean wasPlaying = service.isPlaying();
|
||||
service.forcePause();
|
||||
service.setPausedByTransientLossOfFocus(wasPlaying);
|
||||
break;
|
||||
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
||||
val wasPlaying = service.isPlaying
|
||||
service.pause(true)
|
||||
service.isPausedByTransientLossOfFocus = wasPlaying
|
||||
}
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
// Lost focus for a short time, but it's ok to keep playing
|
||||
// at an attenuated level
|
||||
removeMessages(MusicService.UNDUCK);
|
||||
sendEmptyMessage(DUCK);
|
||||
break;
|
||||
removeMessages(MusicService.UNDUCK)
|
||||
sendEmptyMessage(MusicService.DUCK)
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
mService = WeakReference(service)
|
||||
}
|
||||
}
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
package code.name.monkey.retromusic.service.playback
|
||||
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
|
||||
|
||||
interface Playback {
|
||||
|
||||
|
@ -23,7 +25,9 @@ interface Playback {
|
|||
|
||||
val audioSessionId: Int
|
||||
|
||||
fun setDataSource(path: String, force: Boolean): Boolean
|
||||
fun setDataSource(
|
||||
song: Song, force: Boolean, completion: (success: Boolean) -> Unit,
|
||||
)
|
||||
|
||||
fun setNextDataSource(path: String?)
|
||||
|
||||
|
@ -57,5 +61,7 @@ interface Playback {
|
|||
fun onTrackEnded()
|
||||
|
||||
fun onTrackEndedWithCrossfade()
|
||||
|
||||
fun onPlayStateChanged()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue