package com.plugin.music_notification
import android.app.*
import android.content.Context
import android.content.Intent
import android.media.MediaPlayer
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.media.app.NotificationCompat as MediaNotificationCompat
import androidx.media.session.MediaButtonReceiver
class MusicPlayerService : Service() {
companion object {
private const val NOTIFICATION_ID = 1
private const val CHANNEL_ID = "MusicPlayerChannel"
const val ACTION_PLAY = "com.plugin.music_notification.PLAY"
const val ACTION_PAUSE = "com.plugin.music_notification.PAUSE"
const val ACTION_RESUME = "com.plugin.music_notification.RESUME"
const val ACTION_STOP = "com.plugin.music_notification.STOP"
const val ACTION_NEXT = "com.plugin.music_notification.NEXT"
const val ACTION_PREVIOUS = "com.plugin.music_notification.PREVIOUS"
const val ACTION_SEEK = "com.plugin.music_notification.SEEK"
const val EXTRA_URL = "url"
const val EXTRA_TITLE = "title"
const val EXTRA_ARTIST = "artist"
const val EXTRA_ALBUM = "album"
const val EXTRA_POSITION = "position"
var instance: MusicPlayerService? = null
private val tracks = mutableListOf<TrackInfo>()
private var currentTrackIndex = 0
}
data class TrackInfo(
val url: String,
val title: String,
val artist: String,
val album: String
)
private var mediaPlayer: MediaPlayer? = null
private var isPrepared = false
private lateinit var handler: Handler
private lateinit var progressRunnable: Runnable
private lateinit var mediaSession: MediaSessionCompat
private var currentUrl: String? = null
override fun onCreate() {
super.onCreate()
instance = this
handler = Handler(Looper.getMainLooper())
mediaSession = MediaSessionCompat(this, "MusicPlayerService")
mediaSession.setCallback(object : MediaSessionCompat.Callback() {
override fun onPlay() {
resumeMusic()
}
override fun onPause() {
pauseMusic()
}
override fun onSkipToNext() {
playNextTrack()
}
override fun onSkipToPrevious() {
playPreviousTrack()
}
override fun onStop() {
stopMusic()
}
override fun onSeekTo(pos: Long) {
mediaPlayer?.seekTo(pos.toInt())
updatePlaybackState()
}
})
mediaSession.isActive = true
progressRunnable = object : Runnable {
override fun run() {
mediaPlayer?.let {
if (it.isPlaying) {
updatePlaybackState()
updateNotification()
handler.postDelayed(this, 1000)
}
}
}
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
MediaButtonReceiver.handleIntent(mediaSession, intent)
intent?.let {
when (it.action) {
ACTION_PLAY -> {
val url = it.getStringExtra(EXTRA_URL) ?: return START_STICKY
val title = it.getStringExtra(EXTRA_TITLE) ?: "Unknown Title"
val artist = it.getStringExtra(EXTRA_ARTIST) ?: "Unknown Artist"
val album = it.getStringExtra(EXTRA_ALBUM) ?: "Unknown Album"
playMusic(url, title, artist, album)
}
ACTION_PAUSE -> pauseMusic()
ACTION_RESUME -> resumeMusic()
ACTION_STOP -> stopMusic()
ACTION_NEXT -> playNextTrack()
ACTION_PREVIOUS -> playPreviousTrack()
ACTION_SEEK -> {
val position = it.getLongExtra(EXTRA_POSITION, 0)
mediaPlayer?.seekTo(position.toInt())
updatePlaybackState()
}
}
}
createNotificationChannel()
startForeground(NOTIFICATION_ID, createNotification())
return START_STICKY
}
fun addTrack(url: String, title: String, artist: String, album: String) {
tracks.add(TrackInfo(url, title, artist, album))
}
private fun playMusic(url: String, title: String, artist: String, album: String) {
if (currentUrl == url && mediaPlayer != null && isPrepared) {
resumeMusic()
return
}
mediaPlayer?.apply {
stop()
release()
}
mediaPlayer = null
isPrepared = false
currentUrl = url
mediaPlayer = MediaPlayer().apply {
try {
setDataSource(url)
setOnPreparedListener { mp ->
isPrepared = true
mediaSession.setMetadata(
MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, mp.duration.toLong())
.build()
)
resumeMusic()
}
setOnErrorListener { _, _, _ ->
isPrepared = false
false
}
setOnCompletionListener {
playNextTrack()
}
prepareAsync()
} catch (e: Exception) {
e.printStackTrace()
isPrepared = false
}
}
}
private fun resumeMusic() {
mediaPlayer?.let {
if (isPrepared && !it.isPlaying) {
it.start()
handler.post(progressRunnable)
updatePlaybackState()
updateNotification()
}
}
}
private fun pauseMusic() {
mediaPlayer?.let {
if (it.isPlaying) {
it.pause()
handler.removeCallbacks(progressRunnable)
updatePlaybackState()
updateNotification()
}
}
}
private fun stopMusic() {
handler.removeCallbacks(progressRunnable)
mediaPlayer?.apply {
if (isPlaying) {
stop()
}
release()
}
mediaPlayer = null
isPrepared = false
currentUrl = null
updatePlaybackState()
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
private fun playNextTrack() {
if (tracks.isEmpty()) return
currentTrackIndex = (currentTrackIndex + 1) % tracks.size
val track = tracks[currentTrackIndex]
playMusic(track.url, track.title, track.artist, track.album)
}
private fun playPreviousTrack() {
if (tracks.isEmpty()) return
currentTrackIndex = if (currentTrackIndex - 1 < 0) tracks.size - 1 else currentTrackIndex - 1
val track = tracks[currentTrackIndex]
playMusic(track.url, track.title, track.artist, track.album)
}
private fun createNotification(): Notification {
val controller = mediaSession.controller
val mediaMetadata = controller.metadata
val description = mediaMetadata?.description
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
val isPlaying = mediaPlayer?.isPlaying == true
val playPauseAction = NotificationCompat.Action(
if (isPlaying) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play,
if (isPlaying) "Pausar" else "Reproducir",
MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_PLAY_PAUSE)
)
val previousAction = NotificationCompat.Action(
android.R.drawable.ic_media_previous, "Anterior",
MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)
)
val nextAction = NotificationCompat.Action(
android.R.drawable.ic_media_next, "Siguiente",
MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_SKIP_TO_NEXT)
)
builder.setContentTitle(description?.title ?: "Music Player")
.setContentText(description?.subtitle ?: "")
.setSubText(description?.description ?: "")
.setContentIntent(controller.sessionActivity)
.setDeleteIntent(
MediaButtonReceiver.buildMediaButtonPendingIntent(this, PlaybackStateCompat.ACTION_STOP)
)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setSmallIcon(android.R.drawable.ic_media_play)
.setOngoing(isPlaying)
.addAction(previousAction)
.addAction(playPauseAction)
.addAction(nextAction)
.setStyle(
MediaNotificationCompat.MediaStyle()
.setMediaSession(mediaSession.sessionToken)
.setShowActionsInCompactView(0, 1, 2)
)
return builder.build()
}
private fun updateNotification() {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, createNotification())
}
private fun updatePlaybackState() {
mediaPlayer?.let {
val state = if (it.isPlaying) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED
val position = it.currentPosition.toLong()
val playbackState = PlaybackStateCompat.Builder()
.setActions(
PlaybackStateCompat.ACTION_PLAY_PAUSE or
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
PlaybackStateCompat.ACTION_STOP or
PlaybackStateCompat.ACTION_SEEK_TO
)
.setState(state, position, 1.0f)
.build()
mediaSession.setPlaybackState(playbackState)
}
}
fun getPlaybackState(): Triple<Boolean, Long, Long> {
mediaPlayer?.let {
return Triple(it.isPlaying, it.currentPosition.toLong(), it.duration.toLong())
}
return Triple(false, 0L, 0L)
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
super.onDestroy()
instance = null
handler.removeCallbacks(progressRunnable)
mediaSession.release()
mediaPlayer?.apply {
stop()
release()
}
mediaPlayer = null
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Music Player Channel",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Music playback controls"
setShowBadge(false)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
}
}