Handle playback with PlaybackManager

This commit is contained in:
Prathamesh More 2022-05-13 00:14:34 +05:30
parent b58d971457
commit 70e323eb0a
6 changed files with 617 additions and 698 deletions

View file

@ -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()
}
} }
} }
} }

View file

@ -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();
}
}
}
}

View file

@ -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)
}
}

View file

@ -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)
notHandledMetaChangedForCurrentTrack = false
return prepared
}
fun pause() {
Log.i(TAG, "Paused")
isPausedByTransientLossOfFocus = false
if (playback != null && playback!!.isPlaying) {
startFadeAnimator(playback!!, false) {
//Code to run when Animator Ends
playback?.pause()
notifyChange(PLAY_STATE_CHANGED)
} }
notifyChange(META_CHANGED)
notHandledMetaChangedForCurrentTrack = false
completion(success)
} }
} }
fun forcePause() { fun pause(force: Boolean = false) {
isPausedByTransientLossOfFocus = false isPausedByTransientLossOfFocus = false
if (playback != null && playback!!.isPlaying) { playbackManager.pause(force) {
playback?.pause()
notifyChange(PLAY_STATE_CHANGED) notifyChange(PLAY_STATE_CHANGED)
} }
} }
@ -863,39 +821,26 @@ 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, becomingNoisyReceiverIntentFilter
becomingNoisyReceiverIntentFilter )
) becomingNoisyReceiverRegistered = true
becomingNoisyReceiverRegistered = true
}
if (notHandledMetaChangedForCurrentTrack) {
handleChangeInternal(META_CHANGED)
notHandledMetaChangedForCurrentTrack = false
}
// fixes a bug where the volume would stay ducked because the
// AudioManager.AUDIOFOCUS_GAIN event is not sent
playerHandler?.removeMessages(DUCK)
playerHandler?.sendEmptyMessage(UNDUCK)
}
//Start Playback with Animator
playback?.start()
notifyChange(PLAY_STATE_CHANGED)
} }
if (notHandledMetaChangedForCurrentTrack) {
handleChangeInternal(META_CHANGED)
notHandledMetaChangedForCurrentTrack = false
}
// fixes a bug where the volume would stay ducked because the
// AudioManager.AUDIOFOCUS_GAIN event is not sent
playerHandler?.removeMessages(DUCK)
playerHandler?.sendEmptyMessage(UNDUCK)
} }
notifyChange(PLAY_STATE_CHANGED)
} else { } else {
showToast(R.string.audio_focus_denied) showToast(R.string.audio_focus_denied)
} }
@ -910,16 +855,12 @@ 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() play()
} } else {
showToast(resources.getString(R.string.unplayable_file))
fun playSongAtImpl(position: Int) { }
if (openTrackAndPrepareNextAt(position)) {
play()
} else {
showToast(resources.getString(R.string.unplayable_file))
} }
} }
@ -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
sendChangeInternal(META_CHANGED)
} }
notHandledMetaChangedForCurrentTrack = true
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()
}
} }
} }

View file

@ -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
if (currentDuckVolume > .2f) {
import java.lang.ref.WeakReference; sendEmptyMessageDelayed(MusicService.DUCK, 10)
} else {
import code.name.monkey.retromusic.util.PreferenceUtil; currentDuckVolume = .2f
}
class PlaybackHandler extends Handler { } else {
currentDuckVolume = 1f
@NonNull private final WeakReference<MusicService> mService; }
private float currentDuckVolume = 1.0f; service.playback?.setVolume(currentDuckVolume)
}
MusicService.UNDUCK -> {
PlaybackHandler(final MusicService service, @NonNull final Looper looper) { if (isAudioDucking) {
super(looper); currentDuckVolume += .03f
mService = new WeakReference<>(service); if (currentDuckVolume < 1f) {
} sendEmptyMessageDelayed(MusicService.UNDUCK, 10)
} else {
@Override currentDuckVolume = 1f
public void handleMessage(@NonNull final Message msg) { }
final MusicService service = mService.get(); } else {
if (service == null) { currentDuckVolume = 1f
return; }
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()
}
} else {
service.position = service.nextPosition
service.prepareNextImpl()
service.notifyChange(MusicService.META_CHANGED)
}
MusicService.TRACK_ENDED -> {
// if there is a timer finished, don't continue
if (service.pendingQuit
|| 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()
}
} else {
service.playNextSong(false)
}
sendEmptyMessage(MusicService.RELEASE_WAKELOCK)
}
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
val isAudioFocusEnabled = isAudioFocusEnabled
if (!isAudioFocusEnabled) {
service.pause(true)
}
}
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
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(MusicService.DUCK)
}
}
}
} }
switch (msg.what) { init {
case MusicService.DUCK: mService = WeakReference(service)
if (PreferenceUtil.INSTANCE.isAudioDucking()) {
currentDuckVolume -= .05f;
if (currentDuckVolume > .2f) {
sendEmptyMessageDelayed(DUCK, 10);
} else {
currentDuckVolume = .2f;
}
} else {
currentDuckVolume = 1f;
}
service.playback.setVolume(currentDuckVolume);
break;
case MusicService.UNDUCK:
if (PreferenceUtil.INSTANCE.isAudioDucking()) {
currentDuckVolume += .03f;
if (currentDuckVolume < 1f) {
sendEmptyMessageDelayed(MusicService.UNDUCK, 10);
} else {
currentDuckVolume = 1f;
}
} else {
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);
if (service.pendingQuit) {
service.pendingQuit = false;
service.quit();
break;
}
} else {
service.position = service.nextPosition;
service.prepareNextImpl();
service.notifyChange(META_CHANGED);
}
break;
case 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);
if (service.pendingQuit) {
service.pendingQuit = false;
service.quit();
break;
}
} else {
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);
}
removeMessages(DUCK);
sendEmptyMessage(MusicService.UNDUCK);
break;
case AudioManager.AUDIOFOCUS_LOSS:
// Lost focus for an unbounded amount of time: stop playback and release media playback
boolean isAudioFocusEnabled = PreferenceUtil.INSTANCE.isAudioFocusEnabled();
if (!isAudioFocusEnabled) {
service.forcePause();
}
break;
case 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:
// Lost focus for a short time, but it's ok to keep playing
// at an attenuated level
removeMessages(MusicService.UNDUCK);
sendEmptyMessage(DUCK);
break;
}
break;
} }
}
} }

View file

@ -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()
} }
} }