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.appthemehelper.util.VersionUtils.hasMarshmallow
|
||||||
import code.name.monkey.retromusic.R
|
import code.name.monkey.retromusic.R
|
||||||
import code.name.monkey.retromusic.extensions.showToast
|
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.helper.MusicPlayerRemote
|
||||||
|
import code.name.monkey.retromusic.model.Song
|
||||||
import code.name.monkey.retromusic.service.AudioFader.Companion.createFadeAnimator
|
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
|
||||||
import code.name.monkey.retromusic.service.playback.Playback.PlaybackCallbacks
|
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
|
override val isPlaying: Boolean
|
||||||
get() = mIsInitialized && getCurrentPlayer()?.isPlaying == true
|
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()
|
cancelFade()
|
||||||
if (force) hasDataSource = false
|
if (force) hasDataSource = false
|
||||||
mIsInitialized = false
|
mIsInitialized = false
|
||||||
/* We've already set DataSource if initialized is true in setNextDataSource */
|
/* We've already set DataSource if initialized is true in setNextDataSource */
|
||||||
if (!hasDataSource) {
|
if (!hasDataSource) {
|
||||||
getCurrentPlayer()?.let { mIsInitialized = setDataSourceImpl(it, path) }
|
getCurrentPlayer()?.let {
|
||||||
|
setDataSourceImpl(it, song.uri.toString()) { success ->
|
||||||
|
mIsInitialized = success
|
||||||
|
completion(success)
|
||||||
|
}
|
||||||
|
}
|
||||||
hasDataSource = true
|
hasDataSource = true
|
||||||
} else {
|
} else {
|
||||||
mIsInitialized = true
|
mIsInitialized = true
|
||||||
}
|
}
|
||||||
return mIsInitialized
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setNextDataSource(path: String?) {}
|
override fun setNextDataSource(path: String?) {}
|
||||||
|
@ -148,9 +158,9 @@ class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletion
|
||||||
private fun setDataSourceImpl(
|
private fun setDataSourceImpl(
|
||||||
player: MediaPlayer,
|
player: MediaPlayer,
|
||||||
path: String,
|
path: String,
|
||||||
): Boolean {
|
completion: (success: Boolean) -> Unit,
|
||||||
|
) {
|
||||||
player.reset()
|
player.reset()
|
||||||
player.setOnPreparedListener(null)
|
|
||||||
try {
|
try {
|
||||||
if (path.startsWith("content://")) {
|
if (path.startsWith("content://")) {
|
||||||
player.setDataSource(context, path.toUri())
|
player.setDataSource(context, path.toUri())
|
||||||
|
@ -160,20 +170,18 @@ class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletion
|
||||||
player.setAudioAttributes(
|
player.setAudioAttributes(
|
||||||
AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build()
|
AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build()
|
||||||
)
|
)
|
||||||
player.prepare()
|
player.setPlaybackSpeedPitch(PreferenceUtil.playbackSpeed, PreferenceUtil.playbackPitch)
|
||||||
player.setPlaybackSpeedPitch(playbackSpeed, playbackPitch)
|
player.setOnPreparedListener {
|
||||||
|
player.setOnPreparedListener(null)
|
||||||
|
completion(true)
|
||||||
|
}
|
||||||
|
player.prepareAsync()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
completion(false)
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
player.setOnCompletionListener(this)
|
player.setOnCompletionListener(this)
|
||||||
player.setOnErrorListener(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 {
|
override fun setAudioSessionId(sessionId: Int): Boolean {
|
||||||
|
@ -321,9 +329,11 @@ class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletion
|
||||||
getNextPlayer()?.let { player ->
|
getNextPlayer()?.let { player ->
|
||||||
val nextSong = MusicPlayerRemote.nextSong
|
val nextSong = MusicPlayerRemote.nextSong
|
||||||
if (nextSong != null) {
|
if (nextSong != null) {
|
||||||
setDataSourceImpl(player, MusicUtil.getSongFileUri(nextSong.id).toString())
|
setDataSourceImpl(player,
|
||||||
// Switch to other player / Crossfade only if next song exists
|
MusicUtil.getSongFileUri(nextSong.id).toString()) { success ->
|
||||||
switchPlayer()
|
// 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.graphics.drawable.Drawable
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.media.AudioManager.OnAudioFocusChangeListener
|
import android.media.AudioManager.OnAudioFocusChangeListener
|
||||||
import android.media.audiofx.AudioEffect
|
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.os.Build.VERSION
|
import android.os.Build.VERSION
|
||||||
import android.os.Build.VERSION_CODES
|
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.auto.AutoMusicProvider
|
||||||
import code.name.monkey.retromusic.extensions.showToast
|
import code.name.monkey.retromusic.extensions.showToast
|
||||||
import code.name.monkey.retromusic.extensions.toMediaSessionQueue
|
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.BlurTransformation
|
||||||
import code.name.monkey.retromusic.glide.GlideApp
|
import code.name.monkey.retromusic.glide.GlideApp
|
||||||
import code.name.monkey.retromusic.glide.RetroGlideExtension.getDefaultTransition
|
import code.name.monkey.retromusic.glide.RetroGlideExtension.getDefaultTransition
|
||||||
import code.name.monkey.retromusic.glide.RetroGlideExtension.getSongModel
|
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.helper.ShuffleHelper.makeShuffleList
|
||||||
import code.name.monkey.retromusic.model.Song
|
import code.name.monkey.retromusic.model.Song
|
||||||
import code.name.monkey.retromusic.model.Song.Companion.emptySong
|
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.HistoryStore
|
||||||
import code.name.monkey.retromusic.providers.MusicPlaybackQueueStore
|
import code.name.monkey.retromusic.providers.MusicPlaybackQueueStore
|
||||||
import code.name.monkey.retromusic.providers.SongPlayCountStore
|
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.PlayingNotification
|
||||||
import code.name.monkey.retromusic.service.notification.PlayingNotificationClassic
|
import code.name.monkey.retromusic.service.notification.PlayingNotificationClassic
|
||||||
import code.name.monkey.retromusic.service.notification.PlayingNotificationImpl24
|
import code.name.monkey.retromusic.service.notification.PlayingNotificationImpl24
|
||||||
import code.name.monkey.retromusic.service.playback.Playback
|
import code.name.monkey.retromusic.service.playback.Playback
|
||||||
import code.name.monkey.retromusic.service.playback.Playback.PlaybackCallbacks
|
import code.name.monkey.retromusic.service.playback.Playback.PlaybackCallbacks
|
||||||
import code.name.monkey.retromusic.util.MusicUtil.getMediaStoreAlbumCoverUri
|
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.MusicUtil.toggleFavorite
|
||||||
import code.name.monkey.retromusic.util.PackageValidator
|
import code.name.monkey.retromusic.util.PackageValidator
|
||||||
import code.name.monkey.retromusic.util.PreferenceUtil.crossFadeDuration
|
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.CustomTarget
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
|
import com.google.android.gms.cast.framework.CastSession
|
||||||
import org.koin.java.KoinJavaComponent.get
|
import org.koin.java.KoinJavaComponent.get
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -112,8 +110,10 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
@JvmField
|
@JvmField
|
||||||
var pendingQuit = false
|
var pendingQuit = false
|
||||||
|
|
||||||
@JvmField
|
private lateinit var playbackManager: PlaybackManager
|
||||||
var playback: Playback? = null
|
|
||||||
|
val playback: Playback? get() = playbackManager.playback
|
||||||
|
|
||||||
private var mPackageValidator: PackageValidator? = null
|
private var mPackageValidator: PackageValidator? = null
|
||||||
private val mMusicProvider = get<AutoMusicProvider>(AutoMusicProvider::class.java)
|
private val mMusicProvider = get<AutoMusicProvider>(AutoMusicProvider::class.java)
|
||||||
private var trackEndedByCrossfade = false
|
private var trackEndedByCrossfade = false
|
||||||
|
@ -288,15 +288,10 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
wakeLock?.setReferenceCounted(false)
|
wakeLock?.setReferenceCounted(false)
|
||||||
musicPlayerHandlerThread = HandlerThread("PlaybackHandler")
|
musicPlayerHandlerThread = HandlerThread("PlaybackHandler")
|
||||||
musicPlayerHandlerThread?.start()
|
musicPlayerHandlerThread?.start()
|
||||||
playerHandler = PlaybackHandler(this, musicPlayerHandlerThread!!.looper)
|
playerHandler = PlaybackHandler(this, mainLooper)
|
||||||
|
|
||||||
// Set MultiPlayer when crossfade duration is 0 i.e. off
|
playbackManager = PlaybackManager(this)
|
||||||
playback = if (crossFadeDuration == 0) {
|
playbackManager.setCallbacks(this)
|
||||||
MultiPlayer(this)
|
|
||||||
} else {
|
|
||||||
CrossFadePlayer(this)
|
|
||||||
}
|
|
||||||
playback?.setCallbacks(this)
|
|
||||||
setupMediaSession()
|
setupMediaSession()
|
||||||
|
|
||||||
// queue saving needs to run on a separate thread so that it doesn't block the playback handler
|
// 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
|
val audioSessionId: Int
|
||||||
get() = if (playback != null) {
|
get() = playbackManager.audioSessionId
|
||||||
playback!!.audioSessionId
|
|
||||||
} else -1
|
|
||||||
val currentSong: Song
|
val currentSong: Song
|
||||||
get() = getSongAt(getPosition())
|
get() = getSongAt(getPosition())
|
||||||
|
|
||||||
val nextSong: Song?
|
val nextSong: Song?
|
||||||
get() = if (isLastTrack && repeatMode == REPEAT_MODE_NONE) {
|
get() = if (isLastTrack && repeatMode == REPEAT_MODE_NONE) {
|
||||||
null
|
null
|
||||||
|
@ -501,9 +496,11 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPosition(position: Int) {
|
private fun setPosition(position: Int) {
|
||||||
// handle this on the handlers thread to avoid blocking the ui thread
|
openTrackAndPrepareNextAt(position) { success ->
|
||||||
playerHandler?.removeMessages(SET_POSITION)
|
if (success) {
|
||||||
playerHandler?.obtainMessage(SET_POSITION, position, 0)?.sendToTarget()
|
notifyChange(PLAY_STATE_CHANGED)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPreviousPosition(force: Boolean): Int {
|
private fun getPreviousPosition(force: Boolean): Int {
|
||||||
|
@ -577,13 +574,10 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
val songDurationMillis: Int
|
val songDurationMillis: Int
|
||||||
get() = if (playback != null) {
|
get() = playbackManager.songDurationMillis
|
||||||
playback!!.duration()
|
|
||||||
} else -1
|
|
||||||
val songProgressMillis: Int
|
val songProgressMillis: Int
|
||||||
get() = if (playback != null) {
|
get() = playbackManager.songProgressMillis
|
||||||
playback!!.position()
|
|
||||||
} else -1
|
|
||||||
|
|
||||||
fun handleAndSendChangeInternal(what: String) {
|
fun handleAndSendChangeInternal(what: String) {
|
||||||
handleChangeInternal(what)
|
handleChangeInternal(what)
|
||||||
|
@ -602,8 +596,9 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
|
|
||||||
val isLastTrack: Boolean
|
val isLastTrack: Boolean
|
||||||
get() = getPosition() == playingQueue.size - 1
|
get() = getPosition() == playingQueue.size - 1
|
||||||
|
|
||||||
val isPlaying: Boolean
|
val isPlaying: Boolean
|
||||||
get() = playback != null && playback!!.isPlaying
|
get() = playbackManager.isPlaying
|
||||||
|
|
||||||
fun moveSong(from: Int, to: Int) {
|
fun moveSong(from: Int, to: Int) {
|
||||||
if (from == to) {
|
if (from == to) {
|
||||||
|
@ -702,38 +697,10 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
CROSS_FADE_DURATION -> {
|
CROSS_FADE_DURATION -> {
|
||||||
val progress = songProgressMillis
|
val progress = songProgressMillis
|
||||||
val wasPlaying = isPlaying
|
val wasPlaying = isPlaying
|
||||||
/* Switch to MultiPlayer if Crossfade duration is 0 and
|
|
||||||
Playback is not an instance of MultiPlayer */
|
playbackManager.maybeSwitchToCrossFade(crossFadeDuration)
|
||||||
if (playback !is MultiPlayer && crossFadeDuration == 0) {
|
restorePlaybackState(wasPlaying, progress)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
ALBUM_ART_ON_LOCK_SCREEN, BLURRED_ALBUM_ART -> updateMediaSessionMetaData()
|
ALBUM_ART_ON_LOCK_SCREEN, BLURRED_ALBUM_ART -> updateMediaSessionMetaData()
|
||||||
COLORED_NOTIFICATION -> {
|
COLORED_NOTIFICATION -> {
|
||||||
|
@ -795,6 +762,10 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
playerHandler?.sendEmptyMessage(TRACK_WENT_TO_NEXT)
|
playerHandler?.sendEmptyMessage(TRACK_WENT_TO_NEXT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPlayStateChanged() {
|
||||||
|
notifyChange(PLAY_STATE_CHANGED)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onUnbind(intent: Intent): Boolean {
|
override fun onUnbind(intent: Intent): Boolean {
|
||||||
if (!isPlaying) {
|
if (!isPlaying) {
|
||||||
stopSelf()
|
stopSelf()
|
||||||
|
@ -828,34 +799,21 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun openTrackAndPrepareNextAt(position: Int): Boolean {
|
fun openTrackAndPrepareNextAt(position: Int, completion: (success: Boolean) -> Unit) {
|
||||||
this.position = position
|
this.position = position
|
||||||
val prepared = openCurrent()
|
openCurrent { success ->
|
||||||
if (prepared) {
|
if (success) {
|
||||||
prepareNextImpl()
|
prepareNextImpl()
|
||||||
}
|
}
|
||||||
notifyChange(META_CHANGED)
|
notifyChange(META_CHANGED)
|
||||||
notHandledMetaChangedForCurrentTrack = false
|
notHandledMetaChangedForCurrentTrack = false
|
||||||
return prepared
|
completion(success)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pause() {
|
fun pause(force: Boolean = false) {
|
||||||
Log.i(TAG, "Paused")
|
|
||||||
isPausedByTransientLossOfFocus = false
|
isPausedByTransientLossOfFocus = false
|
||||||
if (playback != null && playback!!.isPlaying) {
|
playbackManager.pause(force) {
|
||||||
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()
|
|
||||||
notifyChange(PLAY_STATE_CHANGED)
|
notifyChange(PLAY_STATE_CHANGED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -863,17 +821,8 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun play() {
|
fun play() {
|
||||||
if (requestFocus()) {
|
if (requestFocus()) {
|
||||||
if (playback != null && !playback!!.isPlaying) {
|
playbackManager.play(onNotInitialized = { playSongAt(getPosition()) }) {
|
||||||
if (!playback!!.isInitialized) {
|
|
||||||
playSongAt(getPosition())
|
|
||||||
} else {
|
|
||||||
//Don't Start playing when it's casting
|
|
||||||
if (isCasting) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
startFadeAnimator(playback!!, true) {
|
|
||||||
|
|
||||||
// Code when Animator Ends
|
|
||||||
if (!becomingNoisyReceiverRegistered) {
|
if (!becomingNoisyReceiverRegistered) {
|
||||||
registerReceiver(
|
registerReceiver(
|
||||||
becomingNoisyReceiver,
|
becomingNoisyReceiver,
|
||||||
|
@ -891,11 +840,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
playerHandler?.removeMessages(DUCK)
|
playerHandler?.removeMessages(DUCK)
|
||||||
playerHandler?.sendEmptyMessage(UNDUCK)
|
playerHandler?.sendEmptyMessage(UNDUCK)
|
||||||
}
|
}
|
||||||
//Start Playback with Animator
|
|
||||||
playback?.start()
|
|
||||||
notifyChange(PLAY_STATE_CHANGED)
|
notifyChange(PLAY_STATE_CHANGED)
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
showToast(R.string.audio_focus_denied)
|
showToast(R.string.audio_focus_denied)
|
||||||
}
|
}
|
||||||
|
@ -910,18 +855,14 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playSongAt(position: Int) {
|
fun playSongAt(position: Int) {
|
||||||
// handle this on the handlers thread to avoid blocking the ui thread
|
openTrackAndPrepareNextAt(position) { success ->
|
||||||
playerHandler?.removeMessages(PLAY_SONG)
|
if (success) {
|
||||||
playerHandler?.obtainMessage(PLAY_SONG, position, 0)?.sendToTarget()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun playSongAtImpl(position: Int) {
|
|
||||||
if (openTrackAndPrepareNextAt(position)) {
|
|
||||||
play()
|
play()
|
||||||
} else {
|
} else {
|
||||||
showToast(resources.getString(R.string.unplayable_file))
|
showToast(resources.getString(R.string.unplayable_file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun playSongs(songs: ArrayList<Song>?, shuffleMode: Int) {
|
fun playSongs(songs: ArrayList<Song>?, shuffleMode: Int) {
|
||||||
if (songs != null && songs.isNotEmpty()) {
|
if (songs != null && songs.isNotEmpty()) {
|
||||||
|
@ -942,7 +883,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
fun prepareNextImpl() {
|
fun prepareNextImpl() {
|
||||||
try {
|
try {
|
||||||
val nextPosition = getNextPosition(false)
|
val nextPosition = getNextPosition(false)
|
||||||
playback?.setNextDataSource(getTrackUri(getSongAt(nextPosition)))
|
playbackManager.setNextDataSource(getSongAt(nextPosition).uri.toString())
|
||||||
this.nextPosition = nextPosition
|
this.nextPosition = nextPosition
|
||||||
} catch (ignored: Exception) {
|
} catch (ignored: Exception) {
|
||||||
}
|
}
|
||||||
|
@ -952,7 +893,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
pause()
|
pause()
|
||||||
stopForeground(true)
|
stopForeground(true)
|
||||||
notificationManager?.cancel(PlayingNotification.NOTIFICATION_ID)
|
notificationManager?.cancel(PlayingNotification.NOTIFICATION_ID)
|
||||||
closeAudioEffectSession()
|
playbackManager.closeAudioEffectSession()
|
||||||
audioManager?.abandonAudioFocus(audioFocusListener)
|
audioManager?.abandonAudioFocus(audioFocusListener)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
@ -1030,13 +971,15 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
originalPlayingQueue = ArrayList(restoredOriginalQueue)
|
originalPlayingQueue = ArrayList(restoredOriginalQueue)
|
||||||
playingQueue = ArrayList(restoredQueue)
|
playingQueue = ArrayList(restoredQueue)
|
||||||
position = restoredPosition
|
position = restoredPosition
|
||||||
openCurrent()
|
openCurrent {
|
||||||
prepareNext()
|
prepareNext()
|
||||||
if (restoredPositionInTrack > 0) {
|
if (restoredPositionInTrack > 0) {
|
||||||
seek(restoredPositionInTrack)
|
seek(restoredPositionInTrack)
|
||||||
}
|
}
|
||||||
notHandledMetaChangedForCurrentTrack = true
|
notHandledMetaChangedForCurrentTrack = true
|
||||||
sendChangeInternal(META_CHANGED)
|
sendChangeInternal(META_CHANGED)
|
||||||
|
}
|
||||||
|
|
||||||
sendChangeInternal(QUEUE_CHANGED)
|
sendChangeInternal(QUEUE_CHANGED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1067,9 +1010,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
fun seek(millis: Int): Int {
|
fun seek(millis: Int): Int {
|
||||||
return try {
|
return try {
|
||||||
var newPosition = 0
|
var newPosition = 0
|
||||||
if (playback != null) {
|
newPosition = playbackManager.seek(millis)
|
||||||
newPosition = playback!!.seek(millis)
|
|
||||||
}
|
|
||||||
throttledSeekHandler?.notifySeek()
|
throttledSeekHandler?.notifySeek()
|
||||||
newPosition
|
newPosition
|
||||||
} catch (e: Exception) {
|
} 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) {
|
private fun handleChangeInternal(what: String) {
|
||||||
when (what) {
|
when (what) {
|
||||||
PLAY_STATE_CHANGED -> {
|
PLAY_STATE_CHANGED -> {
|
||||||
|
@ -1284,23 +1216,39 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
private fun openCurrent(): Boolean {
|
private fun openCurrent(completion: (success: Boolean) -> Unit) {
|
||||||
val force = if (!trackEndedByCrossfade) {
|
val force = if (!trackEndedByCrossfade) {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
trackEndedByCrossfade = false
|
trackEndedByCrossfade = false
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
return try {
|
playbackManager.setDataSource(currentSong, force) { success ->
|
||||||
if (playback != null) {
|
completion(success)
|
||||||
return playback!!.setDataSource(getTrackUri(currentSong), force)
|
|
||||||
} else false
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
private fun playFromPlaylist(intent: Intent) {
|
||||||
val playlist: AbsSmartPlaylist? = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST)
|
val playlist: AbsSmartPlaylist? = intent.getParcelableExtra(INTENT_EXTRA_PLAYLIST)
|
||||||
val shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode())
|
val shuffleMode = intent.getIntExtra(INTENT_EXTRA_SHUFFLE_MODE, getShuffleMode())
|
||||||
|
@ -1347,10 +1295,7 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
musicPlayerHandlerThread?.quitSafely()
|
musicPlayerHandlerThread?.quitSafely()
|
||||||
queueSaveHandler?.removeCallbacksAndMessages(null)
|
queueSaveHandler?.removeCallbacksAndMessages(null)
|
||||||
queueSaveHandlerThread?.quitSafely()
|
queueSaveHandlerThread?.quitSafely()
|
||||||
if (playback != null) {
|
playbackManager.release()
|
||||||
playback?.release()
|
|
||||||
}
|
|
||||||
playback = null
|
|
||||||
mediaSession?.release()
|
mediaSession?.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1508,9 +1453,5 @@ class MusicService : MediaBrowserServiceCompat(),
|
||||||
null
|
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.
|
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
* See the GNU General Public License for more details.
|
* 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;
|
internal class PlaybackHandler(service: MusicService, looper: Looper) : Handler(looper) {
|
||||||
import static code.name.monkey.retromusic.service.MusicService.META_CHANGED;
|
private val mService: WeakReference<MusicService>
|
||||||
import static code.name.monkey.retromusic.service.MusicService.PLAY_STATE_CHANGED;
|
private var currentDuckVolume = 1.0f
|
||||||
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;
|
|
||||||
|
|
||||||
import android.media.AudioManager;
|
override fun handleMessage(msg: Message) {
|
||||||
import android.os.Handler;
|
val service = mService.get() ?: return
|
||||||
import android.os.Looper;
|
when (msg.what) {
|
||||||
import android.os.Message;
|
MusicService.DUCK -> {
|
||||||
|
if (isAudioDucking) {
|
||||||
import androidx.annotation.NonNull;
|
currentDuckVolume -= .05f
|
||||||
|
|
||||||
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;
|
|
||||||
if (currentDuckVolume > .2f) {
|
if (currentDuckVolume > .2f) {
|
||||||
sendEmptyMessageDelayed(DUCK, 10);
|
sendEmptyMessageDelayed(MusicService.DUCK, 10)
|
||||||
} else {
|
} else {
|
||||||
currentDuckVolume = .2f;
|
currentDuckVolume = .2f
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
currentDuckVolume = 1f;
|
currentDuckVolume = 1f
|
||||||
}
|
}
|
||||||
service.playback.setVolume(currentDuckVolume);
|
service.playback?.setVolume(currentDuckVolume)
|
||||||
break;
|
}
|
||||||
|
MusicService.UNDUCK -> {
|
||||||
case MusicService.UNDUCK:
|
if (isAudioDucking) {
|
||||||
if (PreferenceUtil.INSTANCE.isAudioDucking()) {
|
currentDuckVolume += .03f
|
||||||
currentDuckVolume += .03f;
|
|
||||||
if (currentDuckVolume < 1f) {
|
if (currentDuckVolume < 1f) {
|
||||||
sendEmptyMessageDelayed(MusicService.UNDUCK, 10);
|
sendEmptyMessageDelayed(MusicService.UNDUCK, 10)
|
||||||
} else {
|
} else {
|
||||||
currentDuckVolume = 1f;
|
currentDuckVolume = 1f
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
currentDuckVolume = 1f;
|
currentDuckVolume = 1f
|
||||||
}
|
}
|
||||||
service.playback.setVolume(currentDuckVolume);
|
service.playback?.setVolume(currentDuckVolume)
|
||||||
break;
|
}
|
||||||
|
MusicService.TRACK_WENT_TO_NEXT ->
|
||||||
case TRACK_WENT_TO_NEXT:
|
if (service.pendingQuit || service.repeatMode == MusicService.REPEAT_MODE_NONE && service.isLastTrack) {
|
||||||
if (service.pendingQuit
|
service.pause(false)
|
||||||
|| service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) {
|
service.seek(0)
|
||||||
service.pause();
|
|
||||||
service.seek(0);
|
|
||||||
if (service.pendingQuit) {
|
if (service.pendingQuit) {
|
||||||
service.pendingQuit = false;
|
service.pendingQuit = false
|
||||||
service.quit();
|
service.quit()
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
service.position = service.nextPosition;
|
service.position = service.nextPosition
|
||||||
service.prepareNextImpl();
|
service.prepareNextImpl()
|
||||||
service.notifyChange(META_CHANGED);
|
service.notifyChange(MusicService.META_CHANGED)
|
||||||
}
|
}
|
||||||
break;
|
MusicService.TRACK_ENDED -> {
|
||||||
|
|
||||||
case TRACK_ENDED:
|
|
||||||
// if there is a timer finished, don't continue
|
// if there is a timer finished, don't continue
|
||||||
if (service.pendingQuit
|
if (service.pendingQuit
|
||||||
|| service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) {
|
|| service.repeatMode == MusicService.REPEAT_MODE_NONE && service.isLastTrack
|
||||||
service.notifyChange(PLAY_STATE_CHANGED);
|
) {
|
||||||
service.seek(0);
|
service.notifyChange(MusicService.PLAY_STATE_CHANGED)
|
||||||
|
service.seek(0)
|
||||||
if (service.pendingQuit) {
|
if (service.pendingQuit) {
|
||||||
service.pendingQuit = false;
|
service.pendingQuit = false
|
||||||
service.quit();
|
service.quit()
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
service.playNextSong(false);
|
service.playNextSong(false)
|
||||||
}
|
}
|
||||||
sendEmptyMessage(MusicService.RELEASE_WAKELOCK);
|
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);
|
|
||||||
}
|
}
|
||||||
removeMessages(DUCK);
|
MusicService.RELEASE_WAKELOCK -> service.releaseWakeLock()
|
||||||
sendEmptyMessage(MusicService.UNDUCK);
|
MusicService.PREPARE_NEXT -> service.prepareNextImpl()
|
||||||
break;
|
MusicService.RESTORE_QUEUES -> service.restoreQueuesAndPositionIfNecessary()
|
||||||
|
MusicService.FOCUS_CHANGE -> when (msg.arg1) {
|
||||||
case AudioManager.AUDIOFOCUS_LOSS:
|
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
|
// Lost focus for an unbounded amount of time: stop playback and release media playback
|
||||||
boolean isAudioFocusEnabled = PreferenceUtil.INSTANCE.isAudioFocusEnabled();
|
val isAudioFocusEnabled = isAudioFocusEnabled
|
||||||
if (!isAudioFocusEnabled) {
|
if (!isAudioFocusEnabled) {
|
||||||
service.forcePause();
|
service.pause(true)
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
|
||||||
// Lost focus for a short time, but we have to stop
|
// Lost focus for a short time, but we have to stop
|
||||||
// playback. We don't release the media playback because playback
|
// playback. We don't release the media playback because playback
|
||||||
// is likely to resume
|
// is likely to resume
|
||||||
boolean wasPlaying = service.isPlaying();
|
val wasPlaying = service.isPlaying
|
||||||
service.forcePause();
|
service.pause(true)
|
||||||
service.setPausedByTransientLossOfFocus(wasPlaying);
|
service.isPausedByTransientLossOfFocus = wasPlaying
|
||||||
break;
|
}
|
||||||
|
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
|
|
||||||
// Lost focus for a short time, but it's ok to keep playing
|
// Lost focus for a short time, but it's ok to keep playing
|
||||||
// at an attenuated level
|
// at an attenuated level
|
||||||
removeMessages(MusicService.UNDUCK);
|
removeMessages(MusicService.UNDUCK)
|
||||||
sendEmptyMessage(DUCK);
|
sendEmptyMessage(MusicService.DUCK)
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
mService = WeakReference(service)
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
package code.name.monkey.retromusic.service.playback
|
package code.name.monkey.retromusic.service.playback
|
||||||
|
|
||||||
|
import code.name.monkey.retromusic.model.Song
|
||||||
|
|
||||||
|
|
||||||
interface Playback {
|
interface Playback {
|
||||||
|
|
||||||
|
@ -23,7 +25,9 @@ interface Playback {
|
||||||
|
|
||||||
val audioSessionId: Int
|
val audioSessionId: Int
|
||||||
|
|
||||||
fun setDataSource(path: String, force: Boolean): Boolean
|
fun setDataSource(
|
||||||
|
song: Song, force: Boolean, completion: (success: Boolean) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
fun setNextDataSource(path: String?)
|
fun setNextDataSource(path: String?)
|
||||||
|
|
||||||
|
@ -57,5 +61,7 @@ interface Playback {
|
||||||
fun onTrackEnded()
|
fun onTrackEnded()
|
||||||
|
|
||||||
fun onTrackEndedWithCrossfade()
|
fun onTrackEndedWithCrossfade()
|
||||||
|
|
||||||
|
fun onPlayStateChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue