V5 Push
Here's a list of changes/features: https://github.com/RetroMusicPlayer/RetroMusicPlayer/releases/tag/v5.0 Internal Changes: 1) Migrated to ViewBinding 2) Migrated to Glide V4 3) Migrated to kotlin version of Material Dialogs
This commit is contained in:
parent
fc42767031
commit
bce6dbfa27
421 changed files with 13285 additions and 5757 deletions
|
@ -0,0 +1,50 @@
|
|||
package code.name.monkey.retromusic.service
|
||||
|
||||
import code.name.monkey.retromusic.service.playback.Playback
|
||||
import java.util.*
|
||||
|
||||
class AudioFader(
|
||||
private val player: Playback,
|
||||
durationMillis: Long,
|
||||
private val fadeIn: Boolean,
|
||||
private val doOnEnd: Runnable
|
||||
) {
|
||||
val timer = Timer()
|
||||
var volume = if (fadeIn) 0F else 1F
|
||||
val maxVolume = if (fadeIn) 1F else 0F
|
||||
private val volumeStep: Float = PERIOD / durationMillis.toFloat()
|
||||
|
||||
fun start() {
|
||||
timer.scheduleAtFixedRate(
|
||||
object : TimerTask() {
|
||||
override fun run() {
|
||||
setVolume()
|
||||
if (volume < 0 || volume > 1) {
|
||||
player.setVolume(maxVolume)
|
||||
stop()
|
||||
doOnEnd.run()
|
||||
} else {
|
||||
player.setVolume(volume)
|
||||
}
|
||||
}
|
||||
}, 0, PERIOD
|
||||
)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
timer.purge()
|
||||
timer.cancel()
|
||||
}
|
||||
|
||||
private fun setVolume() {
|
||||
if (fadeIn) {
|
||||
volume += volumeStep
|
||||
} else {
|
||||
volume -= volumeStep
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PERIOD = 100L
|
||||
}
|
||||
}
|
|
@ -0,0 +1,406 @@
|
|||
package code.name.monkey.retromusic.service
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaPlayer
|
||||
import android.media.audiofx.AudioEffect
|
||||
import android.net.Uri
|
||||
import android.os.Handler
|
||||
import android.os.Message
|
||||
import android.os.PowerManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.animation.doOnEnd
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
||||
import code.name.monkey.retromusic.service.playback.Playback
|
||||
import code.name.monkey.retromusic.service.playback.Playback.PlaybackCallbacks
|
||||
import code.name.monkey.retromusic.util.MusicUtil
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil
|
||||
|
||||
/** @author Prathamesh M */
|
||||
|
||||
/*
|
||||
* To make Crossfade work we need two MediaPlayer's
|
||||
* Basically, we switch back and forth between those two mp's
|
||||
* e.g. When song is about to end (Reaches Crossfade duration) we let current mediaplayer
|
||||
* play but with decreasing volume and start the player with the next song with increasing volume
|
||||
* and vice versa for upcoming song and so on.
|
||||
*/
|
||||
class CrossFadePlayer(val context: Context) : Playback, MediaPlayer.OnCompletionListener,
|
||||
MediaPlayer.OnErrorListener {
|
||||
|
||||
private var currentPlayer: CurrentPlayer = CurrentPlayer.NOT_SET
|
||||
private var player1 = MediaPlayer()
|
||||
private var player2 = MediaPlayer()
|
||||
private var durationListener = DurationListener()
|
||||
private var trackEndHandledByCrossFade = false
|
||||
private var mIsInitialized = false
|
||||
private var hasDataSource: Boolean = false /* Whether first player has DataSource */
|
||||
private var fadeInAnimator: Animator? = null
|
||||
private var fadeOutAnimator: Animator? = null
|
||||
private var callbacks: PlaybackCallbacks? = null
|
||||
|
||||
init {
|
||||
player1.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
|
||||
player2.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
|
||||
currentPlayer = CurrentPlayer.PLAYER_ONE
|
||||
}
|
||||
|
||||
override fun start(): Boolean {
|
||||
durationListener.start()
|
||||
return try {
|
||||
getCurrentPlayer()?.start()
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
getCurrentPlayer()?.release()
|
||||
getNextPlayer()?.release()
|
||||
durationListener.stop()
|
||||
}
|
||||
|
||||
override fun setCallbacks(callbacks: PlaybackCallbacks) {
|
||||
this.callbacks = callbacks
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
getCurrentPlayer()?.reset()
|
||||
mIsInitialized = false
|
||||
}
|
||||
|
||||
override fun pause(): Boolean {
|
||||
durationListener.stop()
|
||||
cancelFade()
|
||||
getCurrentPlayer()?.let {
|
||||
if (it.isPlaying) {
|
||||
it.pause()
|
||||
}
|
||||
}
|
||||
getNextPlayer()?.let {
|
||||
if (it.isPlaying) {
|
||||
it.pause()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun seek(whereto: Int): Int {
|
||||
cancelFade()
|
||||
getNextPlayer()?.stop()
|
||||
return try {
|
||||
getCurrentPlayer()?.seekTo(whereto)
|
||||
whereto
|
||||
} catch (e: java.lang.IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
override fun setVolume(vol: Float): Boolean {
|
||||
cancelFade()
|
||||
return try {
|
||||
getCurrentPlayer()?.setVolume(vol, vol)
|
||||
true
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override val isInitialized: Boolean
|
||||
get() = mIsInitialized
|
||||
|
||||
override val isPlaying: Boolean
|
||||
get() = mIsInitialized && getCurrentPlayer()?.isPlaying == true
|
||||
|
||||
// This has to run when queue is changed or song is changed manually by user
|
||||
fun sourceChangedByUser() {
|
||||
this.hasDataSource = false
|
||||
cancelFade()
|
||||
getCurrentPlayer()?.apply {
|
||||
if (isPlaying) stop()
|
||||
}
|
||||
getNextPlayer()?.apply {
|
||||
if (isPlaying) stop()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDataSource(path: String): Boolean {
|
||||
cancelFade()
|
||||
mIsInitialized = false
|
||||
/* We've already set DataSource if initialized is true in setNextDataSource */
|
||||
if (!hasDataSource) {
|
||||
getCurrentPlayer()?.let { mIsInitialized = setDataSourceImpl(it, path) }
|
||||
hasDataSource = true
|
||||
} else {
|
||||
mIsInitialized = true
|
||||
}
|
||||
return mIsInitialized
|
||||
}
|
||||
|
||||
override fun setNextDataSource(path: String?) {}
|
||||
|
||||
/**
|
||||
* @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 fun setDataSourceImpl(
|
||||
player: MediaPlayer,
|
||||
path: String
|
||||
): Boolean {
|
||||
player.reset()
|
||||
player.setOnPreparedListener(null)
|
||||
try {
|
||||
if (path.startsWith("content://")) {
|
||||
player.setDataSource(context, Uri.parse(path))
|
||||
} else {
|
||||
player.setDataSource(path)
|
||||
}
|
||||
player.setAudioAttributes(
|
||||
AudioAttributes.Builder().setLegacyStreamType(AudioManager.STREAM_MUSIC).build()
|
||||
)
|
||||
player.prepare()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
player.setOnCompletionListener(this)
|
||||
player.setOnErrorListener(this)
|
||||
val intent = Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
|
||||
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
|
||||
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
|
||||
intent.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
|
||||
context.sendBroadcast(intent)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setAudioSessionId(sessionId: Int): Boolean {
|
||||
return try {
|
||||
getCurrentPlayer()?.audioSessionId = sessionId
|
||||
true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override val audioSessionId: Int
|
||||
get() = getCurrentPlayer()?.audioSessionId!!
|
||||
|
||||
/**
|
||||
* Gets the duration of the file.
|
||||
*
|
||||
* @return The duration in milliseconds
|
||||
*/
|
||||
override fun duration(): Int {
|
||||
return if (!mIsInitialized) {
|
||||
-1
|
||||
} else try {
|
||||
getCurrentPlayer()?.duration!!
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current position in audio.
|
||||
* @return The position in milliseconds
|
||||
*/
|
||||
override fun position(): Int {
|
||||
return if (!mIsInitialized) {
|
||||
-1
|
||||
} else try {
|
||||
getCurrentPlayer()?.currentPosition!!
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTrace()
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompletion(mp: MediaPlayer?) {
|
||||
if (mp == getNextPlayer()) {
|
||||
if (trackEndHandledByCrossFade) {
|
||||
trackEndHandledByCrossFade = false
|
||||
} else {
|
||||
notifyTrackEnded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyTrackEnded(){
|
||||
if (callbacks != null) {
|
||||
callbacks?.onTrackEnded()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrentPlayer(): MediaPlayer? {
|
||||
return when (currentPlayer) {
|
||||
CurrentPlayer.PLAYER_ONE -> {
|
||||
player1
|
||||
}
|
||||
CurrentPlayer.PLAYER_TWO -> {
|
||||
player2
|
||||
}
|
||||
CurrentPlayer.NOT_SET -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNextPlayer(): MediaPlayer? {
|
||||
return when (currentPlayer) {
|
||||
CurrentPlayer.PLAYER_ONE -> {
|
||||
player2
|
||||
}
|
||||
CurrentPlayer.PLAYER_TWO -> {
|
||||
player1
|
||||
}
|
||||
CurrentPlayer.NOT_SET -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fadeIn(mediaPlayer: MediaPlayer) {
|
||||
fadeInAnimator = createFadeAnimator(true, mediaPlayer) {
|
||||
println("Fade In Completed")
|
||||
fadeInAnimator = null
|
||||
}
|
||||
fadeInAnimator?.start()
|
||||
}
|
||||
|
||||
private fun fadeOut(mediaPlayer: MediaPlayer) {
|
||||
fadeOutAnimator = createFadeAnimator(false, mediaPlayer) {
|
||||
println("Fade Out Completed")
|
||||
fadeOutAnimator = null
|
||||
}
|
||||
fadeOutAnimator?.start()
|
||||
}
|
||||
|
||||
private fun cancelFade() {
|
||||
fadeInAnimator?.cancel()
|
||||
fadeOutAnimator?.cancel()
|
||||
fadeInAnimator = null
|
||||
fadeOutAnimator = null
|
||||
getCurrentPlayer()?.setVolume(1f, 1f)
|
||||
getNextPlayer()?.setVolume(0f, 0f)
|
||||
}
|
||||
|
||||
private fun createFadeAnimator(
|
||||
fadeIn: Boolean /* fadeIn -> true fadeOut -> false*/,
|
||||
mediaPlayer: MediaPlayer,
|
||||
callback: Runnable /* Code to run when Animator Ends*/
|
||||
): Animator? {
|
||||
val duration = PreferenceUtil.crossFadeDuration * 1000
|
||||
if (duration == 0) {
|
||||
return null
|
||||
}
|
||||
val startValue = if (fadeIn) 0f else 1.0f
|
||||
val endValue = if (fadeIn) 1.0f else 0f
|
||||
val animator = ValueAnimator.ofFloat(startValue, endValue)
|
||||
animator.duration = duration.toLong()
|
||||
animator.addUpdateListener { animation: ValueAnimator ->
|
||||
mediaPlayer.setVolume(
|
||||
animation.animatedValue as Float, animation.animatedValue as Float
|
||||
)
|
||||
}
|
||||
animator.doOnEnd {
|
||||
callback.run()
|
||||
// Set end values
|
||||
mediaPlayer.setVolume(endValue, endValue)
|
||||
}
|
||||
return animator
|
||||
}
|
||||
|
||||
override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean {
|
||||
mIsInitialized = false
|
||||
mp?.release()
|
||||
player1 = MediaPlayer()
|
||||
player2 = MediaPlayer()
|
||||
mIsInitialized = true
|
||||
mp?.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.resources.getString(R.string.unplayable_file),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
return false
|
||||
}
|
||||
|
||||
enum class CurrentPlayer {
|
||||
PLAYER_ONE,
|
||||
PLAYER_TWO,
|
||||
NOT_SET
|
||||
}
|
||||
|
||||
inner class DurationListener : Handler() {
|
||||
|
||||
fun start() {
|
||||
nextRefresh()
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
removeMessages(DURATION_CHANGED)
|
||||
}
|
||||
|
||||
override fun handleMessage(msg: Message) {
|
||||
super.handleMessage(msg)
|
||||
if (msg.what == DURATION_CHANGED) {
|
||||
nextRefresh()
|
||||
onDurationUpdated(position(), duration())
|
||||
}
|
||||
}
|
||||
|
||||
private fun nextRefresh() {
|
||||
val message = obtainMessage(DURATION_CHANGED)
|
||||
removeMessages(DURATION_CHANGED)
|
||||
sendMessageDelayed(message, 100)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun onDurationUpdated(progress: Int, total: Int) {
|
||||
if (total > 0 && (total - progress).div(1000) == PreferenceUtil.crossFadeDuration) {
|
||||
getNextPlayer()?.let { player ->
|
||||
val nextSong = MusicPlayerRemote.nextSong
|
||||
if (nextSong != null) {
|
||||
setDataSourceImpl(player, MusicUtil.getSongFileUri(nextSong.id).toString())
|
||||
// Switch to other player / Crossfade only if next song exists
|
||||
switchPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchPlayer() {
|
||||
getNextPlayer()?.start()
|
||||
getCurrentPlayer()?.let { fadeOut(it) }
|
||||
getNextPlayer()?.let { fadeIn(it) }
|
||||
currentPlayer =
|
||||
if (currentPlayer == CurrentPlayer.PLAYER_ONE || currentPlayer == CurrentPlayer.NOT_SET) {
|
||||
CurrentPlayer.PLAYER_TWO
|
||||
} else {
|
||||
CurrentPlayer.PLAYER_ONE
|
||||
}
|
||||
notifyTrackEnded()
|
||||
trackEndHandledByCrossFade = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DURATION_CHANGED = 1
|
||||
}
|
||||
}
|
|
@ -18,11 +18,19 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import code.name.monkey.retromusic.auto.AutoMediaIDHelper
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote.cycleRepeatMode
|
||||
import code.name.monkey.retromusic.helper.ShuffleHelper.makeShuffleList
|
||||
import code.name.monkey.retromusic.model.Album
|
||||
import code.name.monkey.retromusic.model.Artist
|
||||
import code.name.monkey.retromusic.model.Playlist
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
import code.name.monkey.retromusic.repository.*
|
||||
import code.name.monkey.retromusic.service.MusicService.*
|
||||
import code.name.monkey.retromusic.util.MusicUtil
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import java.util.*
|
||||
|
||||
|
||||
|
@ -33,7 +41,76 @@ import java.util.*
|
|||
class MediaSessionCallback(
|
||||
private val context: Context,
|
||||
private val musicService: MusicService
|
||||
) : MediaSessionCompat.Callback() {
|
||||
) : MediaSessionCompat.Callback(), KoinComponent {
|
||||
|
||||
private val songRepository by inject<SongRepository>()
|
||||
private val albumRepository by inject<AlbumRepository>()
|
||||
private val artistRepository by inject<ArtistRepository>()
|
||||
private val genreRepository by inject<GenreRepository>()
|
||||
private val playlistRepository by inject<PlaylistRepository>()
|
||||
private val topPlayedRepository by inject<TopPlayedRepository>()
|
||||
|
||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||
super.onPlayFromMediaId(mediaId, extras)
|
||||
val musicId = AutoMediaIDHelper.extractMusicID(mediaId!!)
|
||||
println(musicId)
|
||||
val itemId = musicId?.toLong() ?: -1
|
||||
val songs: ArrayList<Song> = ArrayList()
|
||||
val category = AutoMediaIDHelper.extractCategory(mediaId)
|
||||
when (category) {
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM -> {
|
||||
val album: Album = albumRepository.album(itemId)
|
||||
songs.addAll(album.songs)
|
||||
musicService.openQueue(songs, 0, true)
|
||||
}
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ARTIST -> {
|
||||
val artist: Artist = artistRepository.artist(itemId)
|
||||
songs.addAll(artist.songs)
|
||||
musicService.openQueue(songs, 0, true)
|
||||
}
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_ALBUM_ARTIST -> {
|
||||
val artist: Artist =
|
||||
artistRepository.albumArtist(albumRepository.album(itemId).albumArtist!!)
|
||||
songs.addAll(artist.songs)
|
||||
musicService.openQueue(songs, 0, true)
|
||||
}
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_PLAYLIST -> {
|
||||
val playlist: Playlist = playlistRepository.playlist(itemId)
|
||||
songs.addAll(playlist.getSongs())
|
||||
musicService.openQueue(songs, 0, true)
|
||||
}
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_GENRE -> {
|
||||
songs.addAll(genreRepository.songs(itemId))
|
||||
musicService.openQueue(songs, 0, true)
|
||||
}
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SHUFFLE -> {
|
||||
val allSongs: ArrayList<Song> = songRepository.songs() as ArrayList<Song>
|
||||
makeShuffleList(allSongs, -1)
|
||||
musicService.openQueue(allSongs, 0, true)
|
||||
}
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY,
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SUGGESTIONS,
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS,
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_QUEUE -> {
|
||||
val tracks: List<Song> = when (category) {
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_HISTORY -> topPlayedRepository.recentlyPlayedTracks()
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_SUGGESTIONS -> topPlayedRepository.recentlyPlayedTracks()
|
||||
AutoMediaIDHelper.MEDIA_ID_MUSICS_BY_TOP_TRACKS -> topPlayedRepository.recentlyPlayedTracks()
|
||||
else -> musicService.playingQueue as List<Song>
|
||||
}
|
||||
songs.addAll(tracks)
|
||||
var songIndex = MusicUtil.indexOfSongInList(tracks, itemId)
|
||||
if (songIndex == -1) {
|
||||
songIndex = 0
|
||||
}
|
||||
musicService.openQueue(songs, songIndex, true)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
musicService.play()
|
||||
}
|
||||
|
||||
|
||||
override fun onPlay() {
|
||||
super.onPlay()
|
||||
|
|
|
@ -23,300 +23,302 @@ 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 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();
|
||||
implements Playback, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener {
|
||||
public static final String TAG = MultiPlayer.class.getSimpleName();
|
||||
|
||||
private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
|
||||
private MediaPlayer mNextMediaPlayer;
|
||||
private MediaPlayer mCurrentMediaPlayer = new MediaPlayer();
|
||||
private MediaPlayer mNextMediaPlayer;
|
||||
|
||||
private Context context;
|
||||
@Nullable private Playback.PlaybackCallbacks callbacks;
|
||||
private Context context;
|
||||
@Nullable private Playback.PlaybackCallbacks callbacks;
|
||||
|
||||
private boolean mIsInitialized = false;
|
||||
private boolean mIsInitialized = false;
|
||||
|
||||
/** Constructor of <code>MultiPlayer</code> */
|
||||
MultiPlayer(final Context context) {
|
||||
this.context = context;
|
||||
mCurrentMediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK);
|
||||
}
|
||||
/** 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(@NonNull final String path) {
|
||||
mIsInitialized = false;
|
||||
mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
|
||||
if (mIsInitialized) {
|
||||
setNextDataSource(null);
|
||||
/**
|
||||
* @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(@NonNull final String path) {
|
||||
mIsInitialized = false;
|
||||
mIsInitialized = setDataSourceImpl(mCurrentMediaPlayer, path);
|
||||
if (mIsInitialized) {
|
||||
setNextDataSource(null);
|
||||
}
|
||||
return mIsInitialized;
|
||||
}
|
||||
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);
|
||||
}
|
||||
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)) {
|
||||
/**
|
||||
* @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 {
|
||||
mCurrentMediaPlayer.setNextMediaPlayer(mNextMediaPlayer);
|
||||
} catch (@NonNull IllegalArgumentException | IllegalStateException e) {
|
||||
Log.e(TAG, "setNextDataSource: setNextMediaPlayer()", e);
|
||||
if (mNextMediaPlayer != null) {
|
||||
player.reset();
|
||||
player.setOnPreparedListener(null);
|
||||
if (path.startsWith("content://")) {
|
||||
player.setDataSource(context, Uri.parse(path));
|
||||
} else {
|
||||
player.setDataSource(path);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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();
|
||||
mNextMediaPlayer = null;
|
||||
mNextMediaPlayer.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/** Pauses playback. Call start() to resume. */
|
||||
@Override
|
||||
public boolean pause() {
|
||||
try {
|
||||
mCurrentMediaPlayer.pause();
|
||||
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();
|
||||
/** Checks whether the MultiPlayer is playing. */
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return mIsInitialized && mCurrentMediaPlayer.isPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
/** Pauses playback. Call start() to resume. */
|
||||
@Override
|
||||
public boolean pause() {
|
||||
try {
|
||||
mCurrentMediaPlayer.pause();
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks whether the MultiPlayer is playing. */
|
||||
@Override
|
||||
public boolean isPlaying() {
|
||||
return mIsInitialized && mCurrentMediaPlayer.isPlaying();
|
||||
}
|
||||
/**
|
||||
* 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 duration of the file.
|
||||
*
|
||||
* @return The duration in milliseconds
|
||||
*/
|
||||
@Override
|
||||
public int duration() {
|
||||
if (!mIsInitialized) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
@Override
|
||||
public boolean setVolume(final float vol) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setVolume(vol, vol);
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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;
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setVolume(final float vol) {
|
||||
try {
|
||||
mCurrentMediaPlayer.setVolume(vol, vol);
|
||||
return true;
|
||||
} catch (IllegalStateException e) {
|
||||
return false;
|
||||
/**
|
||||
* Returns the audio session ID.
|
||||
*
|
||||
* @return The current audio session ID.
|
||||
*/
|
||||
@Override
|
||||
public int getAudioSessionId() {
|
||||
return mCurrentMediaPlayer.getAudioSessionId();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/** {@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();
|
||||
}
|
||||
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();
|
||||
/** {@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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,8 +14,15 @@
|
|||
|
||||
package code.name.monkey.retromusic.service;
|
||||
|
||||
import static org.koin.java.KoinJavaComponent.get;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.ALBUM_ART_ON_LOCK_SCREEN;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.BLURRED_ALBUM_ART;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.CLASSIC_NOTIFICATION;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.COLORED_NOTIFICATION;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.CROSS_FADE_DURATION;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.TOGGLE_HEADSET;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.appwidget.AppWidgetManager;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.BroadcastReceiver;
|
||||
|
@ -34,12 +41,14 @@ import android.os.Binder;
|
|||
import android.os.Build;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
import android.os.Process;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.v4.media.MediaBrowserCompat;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
@ -50,11 +59,10 @@ import android.widget.Toast;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media.MediaBrowserServiceCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.bumptech.glide.BitmapRequestBuilder;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.animation.GlideAnimation;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -69,11 +77,13 @@ import code.name.monkey.retromusic.appwidgets.AppWidgetCard;
|
|||
import code.name.monkey.retromusic.appwidgets.AppWidgetClassic;
|
||||
import code.name.monkey.retromusic.appwidgets.AppWidgetSmall;
|
||||
import code.name.monkey.retromusic.appwidgets.AppWidgetText;
|
||||
import code.name.monkey.retromusic.auto.AutoMediaIDHelper;
|
||||
import code.name.monkey.retromusic.auto.AutoMusicProvider;
|
||||
import code.name.monkey.retromusic.glide.BlurTransformation;
|
||||
import code.name.monkey.retromusic.glide.SongGlideRequest;
|
||||
import code.name.monkey.retromusic.glide.GlideApp;
|
||||
import code.name.monkey.retromusic.glide.RetroGlideExtension;
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote;
|
||||
import code.name.monkey.retromusic.helper.ShuffleHelper;
|
||||
import code.name.monkey.retromusic.model.AbsCustomPlaylist;
|
||||
import code.name.monkey.retromusic.model.Playlist;
|
||||
import code.name.monkey.retromusic.model.Song;
|
||||
import code.name.monkey.retromusic.model.smartplaylist.AbsSmartPlaylist;
|
||||
import code.name.monkey.retromusic.providers.HistoryStore;
|
||||
|
@ -84,20 +94,14 @@ import code.name.monkey.retromusic.service.notification.PlayingNotificationImpl;
|
|||
import code.name.monkey.retromusic.service.notification.PlayingNotificationOreo;
|
||||
import code.name.monkey.retromusic.service.playback.Playback;
|
||||
import code.name.monkey.retromusic.util.MusicUtil;
|
||||
import code.name.monkey.retromusic.util.PackageValidator;
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil;
|
||||
import code.name.monkey.retromusic.util.RetroUtil;
|
||||
|
||||
import static code.name.monkey.retromusic.ConstantsKt.ALBUM_ART_ON_LOCK_SCREEN;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.BLURRED_ALBUM_ART;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.CLASSIC_NOTIFICATION;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.COLORED_NOTIFICATION;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.GAP_LESS_PLAYBACK;
|
||||
import static code.name.monkey.retromusic.ConstantsKt.TOGGLE_HEADSET;
|
||||
|
||||
/**
|
||||
* @author Karim Abou Zeid (kabouzeid), Andrew Neal
|
||||
*/
|
||||
public class MusicService extends Service
|
||||
public class MusicService extends MediaBrowserServiceCompat
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener, Playback.PlaybackCallbacks {
|
||||
|
||||
public static final String TAG = MusicService.class.getSimpleName();
|
||||
|
@ -168,6 +172,12 @@ public class MusicService extends Service
|
|||
@Nullable
|
||||
public Playback playback;
|
||||
|
||||
private PackageValidator mPackageValidator;
|
||||
|
||||
private final AutoMusicProvider mMusicProvider = get(AutoMusicProvider.class);
|
||||
|
||||
public boolean trackEndedByCrossfade = false;
|
||||
|
||||
public int position = -1;
|
||||
|
||||
private AppWidgetBig appWidgetBig = AppWidgetBig.Companion.getInstance();
|
||||
|
@ -338,6 +348,7 @@ public class MusicService extends Service
|
|||
private ThrottledSeekHandler throttledSeekHandler;
|
||||
private Handler uiThreadHandler;
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
private AudioFader fader;
|
||||
|
||||
private static Bitmap copy(Bitmap bitmap) {
|
||||
Bitmap.Config config = bitmap.getConfig();
|
||||
|
@ -375,7 +386,13 @@ public class MusicService extends Service
|
|||
musicPlayerHandlerThread.start();
|
||||
playerHandler = new PlaybackHandler(this, musicPlayerHandlerThread.getLooper());
|
||||
|
||||
playback = new MultiPlayer(this);
|
||||
// Set MultiPlayer when crossfade duration is 0 i.e. off
|
||||
if (PreferenceUtil.INSTANCE.getCrossFadeDuration() == 0) {
|
||||
playback = new MultiPlayer(this);
|
||||
} else {
|
||||
playback = new CrossFadePlayer(this);
|
||||
}
|
||||
|
||||
playback.setCallbacks(this);
|
||||
|
||||
setupMediaSession();
|
||||
|
@ -437,6 +454,10 @@ public class MusicService extends Service
|
|||
|
||||
registerHeadsetEvents();
|
||||
registerBluetoothConnected();
|
||||
|
||||
mPackageValidator = new PackageValidator(this, R.xml.allowed_media_browser_callers);
|
||||
mMusicProvider.setMusicService(this);
|
||||
setSessionToken(mediaSession.getSessionToken());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -536,6 +557,14 @@ public class MusicService extends Service
|
|||
return getSongAt(getPosition());
|
||||
}
|
||||
|
||||
public Song getNextSong() {
|
||||
if (!isLastTrack() || getRepeatMode() == REPEAT_MODE_THIS) {
|
||||
return getSongAt(getNextPosition(false));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MediaSessionCompat getMediaSession() {
|
||||
return mediaSession;
|
||||
|
@ -764,19 +793,68 @@ public class MusicService extends Service
|
|||
@NonNull
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
// For Android auto, need to call super, or onGetRoot won't be called.
|
||||
if (intent != null && "android.media.browse.MediaBrowserService".equals(intent.getAction())) {
|
||||
return super.onBind(intent);
|
||||
}
|
||||
return musicBind;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
|
||||
|
||||
// Check origin to ensure we're not allowing any arbitrary app to browse app contents
|
||||
if (!mPackageValidator.isKnownCaller(clientPackageName, clientUid)) {
|
||||
// Request from an untrusted package: return an empty browser root
|
||||
return new BrowserRoot(AutoMediaIDHelper.MEDIA_ID_EMPTY_ROOT, null);
|
||||
}
|
||||
|
||||
return new BrowserRoot(AutoMediaIDHelper.MEDIA_ID_ROOT, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadChildren(@NonNull String parentId, @NonNull MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>> result) {
|
||||
result.sendResult(mMusicProvider.getChildren(parentId, getResources()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(
|
||||
@NonNull SharedPreferences sharedPreferences, @NonNull String key) {
|
||||
switch (key) {
|
||||
case GAP_LESS_PLAYBACK:
|
||||
if (sharedPreferences.getBoolean(key, false)) {
|
||||
prepareNext();
|
||||
} else {
|
||||
case CROSS_FADE_DURATION:
|
||||
int progress = getSongProgressMillis();
|
||||
boolean wasPlaying = isPlaying();
|
||||
/* Switch to MultiPlayer if Crossfade duration is 0 and
|
||||
Playback is not an instance of MultiPlayer */
|
||||
if (!(playback instanceof MultiPlayer) && PreferenceUtil.INSTANCE.getCrossFadeDuration() == 0) {
|
||||
if (playback != null) {
|
||||
playback.setNextDataSource(null);
|
||||
playback.release();
|
||||
}
|
||||
playback = null;
|
||||
playback = new MultiPlayer(this);
|
||||
playback.setCallbacks(this);
|
||||
if (openTrackAndPrepareNextAt(position)) {
|
||||
seek(progress);
|
||||
if (wasPlaying) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Switch to CrossFadePlayer if Crossfade duration is greater than 0 and
|
||||
Playback is not an instance of CrossFadePlayer */
|
||||
else if (!(playback instanceof CrossFadePlayer) && PreferenceUtil.INSTANCE.getCrossFadeDuration() > 0) {
|
||||
if (playback != null) {
|
||||
playback.release();
|
||||
}
|
||||
playback = null;
|
||||
playback = new CrossFadePlayer(this);
|
||||
playback.setCallbacks(this);
|
||||
if (openTrackAndPrepareNextAt(position)) {
|
||||
seek(progress);
|
||||
if (wasPlaying) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -901,6 +979,22 @@ public class MusicService extends Service
|
|||
}
|
||||
|
||||
public void pause() {
|
||||
pausedByTransientLossOfFocus = false;
|
||||
if (playback != null && playback.isPlaying()) {
|
||||
if (fader != null) {
|
||||
fader.stop();
|
||||
}
|
||||
fader = new AudioFader(playback, PreferenceUtil.INSTANCE.getAudioFadeDuration(), false, () -> {
|
||||
if (playback != null && playback.isPlaying()) {
|
||||
playback.pause();
|
||||
notifyChange(PLAY_STATE_CHANGED);
|
||||
}
|
||||
});
|
||||
fader.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void forcePause() {
|
||||
pausedByTransientLossOfFocus = false;
|
||||
if (playback != null && playback.isPlaying()) {
|
||||
playback.pause();
|
||||
|
@ -915,21 +1009,31 @@ public class MusicService extends Service
|
|||
if (!playback.isInitialized()) {
|
||||
playSongAt(getPosition());
|
||||
} else {
|
||||
playback.start();
|
||||
if (!becomingNoisyReceiverRegistered) {
|
||||
registerReceiver(becomingNoisyReceiver, becomingNoisyReceiverIntentFilter);
|
||||
becomingNoisyReceiverRegistered = true;
|
||||
//Don't Start playing when it's casting
|
||||
if (MusicPlayerRemote.INSTANCE.isCasting()) {
|
||||
return;
|
||||
}
|
||||
if (notHandledMetaChangedForCurrentTrack) {
|
||||
handleChangeInternal(META_CHANGED);
|
||||
notHandledMetaChangedForCurrentTrack = false;
|
||||
if (fader != null) {
|
||||
fader.stop();
|
||||
}
|
||||
notifyChange(PLAY_STATE_CHANGED);
|
||||
fader = new AudioFader(playback, PreferenceUtil.INSTANCE.getAudioFadeDuration(), false, () -> {
|
||||
if (!becomingNoisyReceiverRegistered) {
|
||||
registerReceiver(becomingNoisyReceiver, becomingNoisyReceiverIntentFilter);
|
||||
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);
|
||||
// fixes a bug where the volume would stay ducked because the
|
||||
// AudioManager.AUDIOFOCUS_GAIN event is not sent
|
||||
playerHandler.removeMessages(DUCK);
|
||||
playerHandler.sendEmptyMessage(UNDUCK);
|
||||
});
|
||||
playback.start();
|
||||
notifyChange(PLAY_STATE_CHANGED);
|
||||
fader.start();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -955,6 +1059,14 @@ public class MusicService extends Service
|
|||
}
|
||||
|
||||
public void playSongAtImpl(int position) {
|
||||
if (!trackEndedByCrossfade) {
|
||||
// This is only imp if we are using crossfade
|
||||
if (playback instanceof CrossFadePlayer) {
|
||||
((CrossFadePlayer) playback).sourceChangedByUser();
|
||||
}
|
||||
} else {
|
||||
trackEndedByCrossfade = false;
|
||||
}
|
||||
if (openTrackAndPrepareNextAt(position)) {
|
||||
play();
|
||||
} else {
|
||||
|
@ -978,7 +1090,7 @@ public class MusicService extends Service
|
|||
}
|
||||
}
|
||||
|
||||
public boolean prepareNextImpl() {
|
||||
public void prepareNextImpl() {
|
||||
synchronized (this) {
|
||||
try {
|
||||
int nextPosition = getNextPosition(false);
|
||||
|
@ -986,9 +1098,7 @@ public class MusicService extends Service
|
|||
playback.setNextDataSource(getTrackUri(Objects.requireNonNull(getSongAt(nextPosition))));
|
||||
}
|
||||
this.nextPosition = nextPosition;
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1176,9 +1286,7 @@ public class MusicService extends Service
|
|||
|
||||
if (PreferenceUtil.INSTANCE.isAlbumArtOnLockScreen()) {
|
||||
final Point screenSize = RetroUtil.getScreenSize(MusicService.this);
|
||||
final BitmapRequestBuilder<?, Bitmap> request = SongGlideRequest.Builder.from(Glide.with(MusicService.this), song)
|
||||
.checkIgnoreMediaStore(MusicService.this)
|
||||
.asBitmap().build();
|
||||
final RequestBuilder<Bitmap> request = GlideApp.with(MusicService.this).asBitmap().songCoverOptions(song).load(RetroGlideExtension.INSTANCE.getSongModel(song));
|
||||
if (PreferenceUtil.INSTANCE.isBlurredAlbumArt()) {
|
||||
request.transform(new BlurTransformation.Builder(MusicService.this).build());
|
||||
}
|
||||
|
@ -1187,14 +1295,13 @@ public class MusicService extends Service
|
|||
public void run() {
|
||||
request.into(new SimpleTarget<Bitmap>(screenSize.x, screenSize.y) {
|
||||
@Override
|
||||
public void onLoadFailed(Exception e, Drawable errorDrawable) {
|
||||
super.onLoadFailed(e, errorDrawable);
|
||||
public void onLoadFailed(Drawable errorDrawable) {
|
||||
super.onLoadFailed(errorDrawable);
|
||||
mediaSession.setMetadata(metaData.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
|
||||
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable com.bumptech.glide.request.transition.Transition<? super Bitmap> transition) {
|
||||
metaData.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, copy(resource));
|
||||
mediaSession.setMetadata(metaData.build());
|
||||
}
|
||||
|
@ -1238,6 +1345,7 @@ public class MusicService extends Service
|
|||
case META_CHANGED:
|
||||
updateNotification();
|
||||
updateMediaSessionMetaData();
|
||||
updateMediaSessionPlaybackState();
|
||||
savePosition();
|
||||
savePositionInTrack();
|
||||
final Song currentSong = getCurrentSong();
|
||||
|
@ -1266,6 +1374,7 @@ public class MusicService extends Service
|
|||
return playback.setDataSource(getTrackUri(Objects.requireNonNull(getCurrentSong())));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,10 +25,13 @@ import android.media.AudioManager;
|
|||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil;
|
||||
|
||||
class PlaybackHandler extends Handler {
|
||||
|
||||
@NonNull private final WeakReference<MusicService> mService;
|
||||
|
@ -93,6 +96,7 @@ class PlaybackHandler extends Handler {
|
|||
break;
|
||||
|
||||
case TRACK_ENDED:
|
||||
service.trackEndedByCrossfade = true;
|
||||
// if there is a timer finished, don't continue
|
||||
if (service.pendingQuit
|
||||
|| service.getRepeatMode() == REPEAT_MODE_NONE && service.isLastTrack()) {
|
||||
|
@ -143,7 +147,7 @@ class PlaybackHandler extends Handler {
|
|||
|
||||
case AudioManager.AUDIOFOCUS_LOSS:
|
||||
// Lost focus for an unbounded amount of time: stop playback and release media playback
|
||||
service.pause();
|
||||
service.forcePause();
|
||||
break;
|
||||
|
||||
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
|
||||
|
@ -151,7 +155,7 @@ class PlaybackHandler extends Handler {
|
|||
// playback. We don't release the media playback because playback
|
||||
// is likely to resume
|
||||
boolean wasPlaying = service.isPlaying();
|
||||
service.pause();
|
||||
service.forcePause();
|
||||
service.setPausedByTransientLossOfFocus(wasPlaying);
|
||||
break;
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ import code.name.monkey.retromusic.R
|
|||
import code.name.monkey.retromusic.activities.MainActivity
|
||||
import code.name.monkey.retromusic.db.PlaylistEntity
|
||||
import code.name.monkey.retromusic.db.toSongEntity
|
||||
import code.name.monkey.retromusic.glide.SongGlideRequest
|
||||
import code.name.monkey.retromusic.glide.GlideApp
|
||||
import code.name.monkey.retromusic.glide.RetroGlideExtension
|
||||
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
|
||||
import code.name.monkey.retromusic.service.MusicService
|
||||
import code.name.monkey.retromusic.service.MusicService.*
|
||||
|
@ -37,9 +38,9 @@ import code.name.monkey.retromusic.util.MusicUtil
|
|||
import code.name.monkey.retromusic.util.PreferenceUtil
|
||||
import code.name.monkey.retromusic.util.RetroColorUtil
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.animation.GlideAnimation
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.KoinComponent
|
||||
|
@ -79,11 +80,11 @@ class PlayingNotificationImpl : PlayingNotification(), KoinComponent {
|
|||
.getDimensionPixelSize(R.dimen.notification_big_image_size)
|
||||
service.runOnUiThread {
|
||||
if (target != null) {
|
||||
Glide.clear(target)
|
||||
Glide.with(service).clear(target)
|
||||
}
|
||||
target = SongGlideRequest.Builder.from(Glide.with(service), song)
|
||||
.checkIgnoreMediaStore(service)
|
||||
.generatePalette(service).build()
|
||||
target = GlideApp.with(service).asBitmapPalette().songCoverOptions(song)
|
||||
.load(RetroGlideExtension.getSongModel(song))
|
||||
//.checkIgnoreMediaStore()
|
||||
.centerCrop()
|
||||
.into(object : SimpleTarget<BitmapPaletteWrapper>(
|
||||
bigNotificationImageSize,
|
||||
|
@ -91,7 +92,7 @@ class PlayingNotificationImpl : PlayingNotification(), KoinComponent {
|
|||
) {
|
||||
override fun onResourceReady(
|
||||
resource: BitmapPaletteWrapper,
|
||||
glideAnimation: GlideAnimation<in BitmapPaletteWrapper>
|
||||
transition: Transition<in BitmapPaletteWrapper>?
|
||||
) {
|
||||
update(
|
||||
resource.bitmap,
|
||||
|
@ -99,8 +100,8 @@ class PlayingNotificationImpl : PlayingNotification(), KoinComponent {
|
|||
)
|
||||
}
|
||||
|
||||
override fun onLoadFailed(e: Exception?, errorDrawable: Drawable?) {
|
||||
super.onLoadFailed(e, errorDrawable)
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
super.onLoadFailed(errorDrawable)
|
||||
update(null, Color.TRANSPARENT)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,8 @@ import code.name.monkey.appthemehelper.util.ColorUtil
|
|||
import code.name.monkey.appthemehelper.util.MaterialValueHelper
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.activities.MainActivity
|
||||
import code.name.monkey.retromusic.glide.SongGlideRequest
|
||||
import code.name.monkey.retromusic.glide.GlideApp
|
||||
import code.name.monkey.retromusic.glide.RetroGlideExtension
|
||||
import code.name.monkey.retromusic.glide.palette.BitmapPaletteWrapper
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
import code.name.monkey.retromusic.service.MusicService
|
||||
|
@ -38,9 +39,9 @@ import code.name.monkey.retromusic.util.RetroUtil
|
|||
import code.name.monkey.retromusic.util.RetroUtil.createBitmap
|
||||
import code.name.monkey.retromusic.util.color.MediaNotificationProcessor
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.animation.GlideAnimation
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
|
||||
/**
|
||||
* @author Hemanth S (h4h13).
|
||||
|
@ -95,11 +96,10 @@ class PlayingNotificationOreo : PlayingNotification() {
|
|||
.getDimensionPixelSize(R.dimen.notification_big_image_size)
|
||||
service.runOnUiThread {
|
||||
if (target != null) {
|
||||
Glide.clear(target)
|
||||
Glide.with(service).clear(target)
|
||||
}
|
||||
target = SongGlideRequest.Builder.from(Glide.with(service), song)
|
||||
.checkIgnoreMediaStore(service)
|
||||
.generatePalette(service).build()
|
||||
target = GlideApp.with(service).asBitmapPalette().songCoverOptions(song)
|
||||
.load(RetroGlideExtension.getSongModel(song))
|
||||
.centerCrop()
|
||||
.into(object : SimpleTarget<BitmapPaletteWrapper>(
|
||||
bigNotificationImageSize,
|
||||
|
@ -107,7 +107,7 @@ class PlayingNotificationOreo : PlayingNotification() {
|
|||
) {
|
||||
override fun onResourceReady(
|
||||
resource: BitmapPaletteWrapper,
|
||||
glideAnimation: GlideAnimation<in BitmapPaletteWrapper>
|
||||
transition: Transition<in BitmapPaletteWrapper>?
|
||||
) {
|
||||
/* val mediaNotificationProcessor = MediaNotificationProcessor(
|
||||
service,
|
||||
|
@ -119,8 +119,8 @@ class PlayingNotificationOreo : PlayingNotification() {
|
|||
update(resource.bitmap, colors.backgroundColor)
|
||||
}
|
||||
|
||||
override fun onLoadFailed(e: Exception?, errorDrawable: Drawable?) {
|
||||
super.onLoadFailed(e, errorDrawable)
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
super.onLoadFailed(errorDrawable)
|
||||
update(
|
||||
null,
|
||||
resolveColor(service, R.attr.colorSurface, Color.WHITE)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue