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.cast.CastPlayer import androidx.media3.cast.SessionAvailabilityListener import androidx.media3.common.* import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory 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.UIUtil import com.google.android.gms.cast.framework.CastContext import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture @UnstableApi class MediaService : MediaLibraryService(), SessionAvailabilityListener { private val librarySessionCallback = CustomMediaLibrarySessionCallback() private lateinit var player: ExoPlayer private lateinit var castPlayer: CastPlayer 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() initializeCastPlayer() initializeMediaLibrarySession() initializePlayerListener() setPlayer( null, if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else 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 onGetLibraryRoot( session: MediaLibrarySession, browser: ControllerInfo, params: LibraryParams? ): ListenableFuture> { return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params)) } override fun onGetItem( session: MediaLibrarySession, browser: ControllerInfo, mediaId: String ): ListenableFuture> { val item = MediaItemTree.getItem(mediaId) ?: return Futures.immediateFuture( LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) ) return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null)) } override fun onSubscribe( session: MediaLibrarySession, browser: ControllerInfo, parentId: String, params: LibraryParams? ): ListenableFuture> { val children = MediaItemTree.getChildren(parentId) ?: return Futures.immediateFuture( LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) ) session.notifyChildrenChanged(browser, parentId, children.size, params) return Futures.immediateFuture(LibraryResult.ofVoid()) } override fun onGetChildren( session: MediaLibrarySession, browser: ControllerInfo, parentId: String, page: Int, pageSize: Int, params: LibraryParams? ): ListenableFuture>> { val children = MediaItemTree.getChildren(parentId) ?: return Futures.immediateFuture( LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) ) return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) }*/ 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) .build() } private fun initializeCastPlayer() { if (UIUtil.isCastApiAvailable(this)) { castPlayer = CastPlayer(CastContext.getSharedInstance(this)) castPlayer.setSessionAvailabilityListener(this) } } 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 onIsPlayingChanged(isPlaying: Boolean) { if (!isPlaying) { MediaManager.setPlayingPausedTimestamp( player.currentMediaItem, player.currentPosition ) } } 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) MediaManager.saveChronology(oldPosition.mediaItem) } if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) { MediaManager.setLastPlayedTimestamp(newPosition.mediaItem) } } } }) } private fun setPlayer(oldPlayer: Player?, newPlayer: Player) { if (oldPlayer === newPlayer) return oldPlayer?.stop() mediaLibrarySession.player = newPlayer } private fun releasePlayer() { if (this::castPlayer.isInitialized) castPlayer.setSessionAvailabilityListener(null) if (this::castPlayer.isInitialized) castPlayer.release() 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 getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) private fun getMediaSourceFactory() = DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this)) override fun onCastSessionAvailable() { setPlayer(player, castPlayer) } override fun onCastSessionUnavailable() { setPlayer(castPlayer, player) } }