package com.cappielloantonio.tempo.service import android.annotation.SuppressLint import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.TaskStackBuilder import android.content.Intent import android.os.Bundle import androidx.media3.common.* import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.TrackGroupArray import androidx.media3.exoplayer.trackselection.TrackSelectionArray import androidx.media3.session.* import androidx.media3.session.MediaSession.ControllerInfo import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.Constants import com.cappielloantonio.tempo.util.DownloadUtil import com.cappielloantonio.tempo.util.Preferences import com.cappielloantonio.tempo.util.ReplayGainUtil import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @UnstableApi class MediaService : MediaLibraryService() { private val librarySessionCallback = CustomMediaLibrarySessionCallback() private lateinit var player: ExoPlayer private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var customCommands: List private var customLayout = ImmutableList.of() companion object { private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON = "android.media3.session.demo.SHUFFLE_ON" private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = "android.media3.session.demo.SHUFFLE_OFF" } override fun onCreate() { super.onCreate() initializeCustomCommands() initializePlayer() initializeMediaLibrarySession() initializePlayerListener() setPlayer(player) } override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession { return mediaLibrarySession } override fun onDestroy() { releasePlayer() super.onDestroy() } private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback { override fun onConnect( session: MediaSession, controller: ControllerInfo ): MediaSession.ConnectionResult { val connectionResult = super.onConnect(session, controller) val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() customCommands.forEach { commandButton -> // TODO: Aggiungere i comandi personalizzati // commandButton.sessionCommand?.let { availableSessionCommands.add(it) } } return MediaSession.ConnectionResult.accept( availableSessionCommands.build(), connectionResult.availablePlayerCommands ) } override fun onPostConnect(session: MediaSession, controller: ControllerInfo) { if (!customLayout.isEmpty() && controller.controllerVersion != 0) { ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout)) } } override fun onCustomCommand( session: MediaSession, controller: ControllerInfo, customCommand: SessionCommand, args: Bundle ): ListenableFuture { if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) { player.shuffleModeEnabled = true customLayout = ImmutableList.of(customCommands[1]) session.setCustomLayout(customLayout) } else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) { player.shuffleModeEnabled = false customLayout = ImmutableList.of(customCommands[0]) session.setCustomLayout(customLayout) } return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } override fun onAddMediaItems( mediaSession: MediaSession, controller: ControllerInfo, mediaItems: List ): ListenableFuture> { val updatedMediaItems = mediaItems.map { it.buildUpon() .setUri(it.requestMetadata.mediaUri) .setMediaMetadata(it.mediaMetadata) .setMimeType(MimeTypes.BASE_TYPE_AUDIO) .build() } return Futures.immediateFuture(updatedMediaItems) } } private fun initializeCustomCommands() { customCommands = listOf( getShuffleCommandButton( SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY) ), getShuffleCommandButton( SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY) ) ) customLayout = ImmutableList.of(customCommands[0]) } private fun initializePlayer() { player = ExoPlayer.Builder(this) .setRenderersFactory(getRenderersFactory()) .setMediaSourceFactory(getMediaSourceFactory()) .setAudioAttributes(AudioAttributes.DEFAULT, true) .setHandleAudioBecomingNoisy(true) .setWakeMode(C.WAKE_MODE_NETWORK) .setLoadControl(initializeLoadControl()) .build() } private fun initializeMediaLibrarySession() { val sessionActivityPendingIntent = TaskStackBuilder.create(this).run { addNextIntent(Intent(this@MediaService, MainActivity::class.java)) getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT) } mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) .setSessionActivity(sessionActivityPendingIntent) .build() if (!customLayout.isEmpty()) { mediaLibrarySession.setCustomLayout(customLayout) } } private fun initializePlayerListener() { player.addListener(object : Player.Listener { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { if (mediaItem == null) return if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) { MediaManager.setLastPlayedTimestamp(mediaItem) } } override fun onTracksChanged(tracks: Tracks) { ReplayGainUtil.setReplayGain(player, tracks) MediaManager.scrobble(player.currentMediaItem, false) } override fun onIsPlayingChanged(isPlaying: Boolean) { if (!isPlaying) { MediaManager.setPlayingPausedTimestamp( player.currentMediaItem, player.currentPosition ) } else { MediaManager.scrobble(player.currentMediaItem, false) } } override fun onPlaybackStateChanged(playbackState: Int) { super.onPlaybackStateChanged(playbackState) if (!player.hasNextMediaItem() && playbackState == Player.STATE_ENDED && player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC ) { MediaManager.scrobble(player.currentMediaItem, true) MediaManager.saveChronology(player.currentMediaItem) } } override fun onPositionDiscontinuity( oldPosition: Player.PositionInfo, newPosition: Player.PositionInfo, reason: Int ) { super.onPositionDiscontinuity(oldPosition, newPosition, reason) if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { MediaManager.scrobble(oldPosition.mediaItem, true) MediaManager.saveChronology(oldPosition.mediaItem) } if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { MediaManager.setLastPlayedTimestamp(newPosition.mediaItem) } } } }) } private fun setPlayer(player: Player) { mediaLibrarySession.player = player } private fun releasePlayer() { player.release() mediaLibrarySession.release() } @SuppressLint("PrivateResource") private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton { val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON return CommandButton.Builder() .setDisplayName( getString( if (isOn) R.string.exo_controls_shuffle_on_description else R.string.exo_controls_shuffle_off_description ) ) .setSessionCommand(sessionCommand) .setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on) .build() } private fun ignoreFuture(customLayout: ListenableFuture) { /* Do nothing. */ } private fun initializeLoadControl(): DefaultLoadControl { return DefaultLoadControl.Builder() .setBufferDurationsMs( (DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(), (DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(), DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS ) .build() } private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) private fun getMediaSourceFactory() = DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this)) }