From fc9f4054dd944658b131cf0efcd94d64a2b951c5 Mon Sep 17 00:00:00 2001 From: antonio Date: Fri, 26 Aug 2022 18:14:29 +0200 Subject: [PATCH] The service that manages the music player has been rewritten in kotlin --- app/src/main/AndroidManifest.xml | 1 + .../play/service/MediaService.java | 144 --------- .../play/service/MediaService.kt | 280 ++++++++++++++++++ 3 files changed, 281 insertions(+), 144 deletions(-) delete mode 100644 app/src/main/java/com/cappielloantonio/play/service/MediaService.java create mode 100644 app/src/main/java/com/cappielloantonio/play/service/MediaService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 52e76915..4b167818 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,7 @@ diff --git a/app/src/main/java/com/cappielloantonio/play/service/MediaService.java b/app/src/main/java/com/cappielloantonio/play/service/MediaService.java deleted file mode 100644 index 15146401..00000000 --- a/app/src/main/java/com/cappielloantonio/play/service/MediaService.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.cappielloantonio.play.service; - -import android.annotation.SuppressLint; -import android.app.PendingIntent; -import android.content.Intent; - -import androidx.annotation.Nullable; -import androidx.media3.cast.CastPlayer; -import androidx.media3.cast.SessionAvailabilityListener; -import androidx.media3.common.AudioAttributes; -import androidx.media3.common.C; -import androidx.media3.common.MediaItem; -import androidx.media3.common.MimeTypes; -import androidx.media3.common.Player; -import androidx.media3.datasource.DataSource; -import androidx.media3.exoplayer.ExoPlayer; -import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; -import androidx.media3.session.MediaLibraryService; -import androidx.media3.session.MediaSession; - -import com.cappielloantonio.play.model.Media; -import com.cappielloantonio.play.ui.activity.MainActivity; -import com.cappielloantonio.play.util.DownloadUtil; -import com.google.android.gms.cast.framework.CastContext; - -@SuppressLint("UnsafeOptInUsageError") -public class MediaService extends MediaLibraryService implements SessionAvailabilityListener { - private static final String TAG = "MediaService"; - - public static final int REQUEST_CODE = 432; - - private ExoPlayer player; - private CastPlayer castPlayer; - private DefaultMediaSourceFactory defaultMediaSourceFactory; - private MediaLibrarySession mediaLibrarySession; - - @Override - public void onCreate() { - super.onCreate(); - initializeMediaSource(); - initializePlayer(); - initializeCastPlayer(); - initializeMediaLibrarySession(); - initializePlayerListener(); - - setPlayer(null, castPlayer.isCastSessionAvailable() ? castPlayer : player); - } - - @Override - public void onDestroy() { - releasePlayer(); - super.onDestroy(); - } - - @Nullable - @Override - public MediaLibrarySession onGetSession(MediaSession.ControllerInfo controllerInfo) { - return mediaLibrarySession; - } - - private void initializeMediaSource() { - DataSource.Factory dataSourceFactory = DownloadUtil.getDataSourceFactory(this); - defaultMediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory); - } - - private void initializePlayer() { - player = new ExoPlayer.Builder(this) - .setMediaSourceFactory(defaultMediaSourceFactory) - .setAudioAttributes(AudioAttributes.DEFAULT, true) - .setHandleAudioBecomingNoisy(true) - .setWakeMode(C.WAKE_MODE_NETWORK) - .build(); - } - - private void initializeCastPlayer() { - castPlayer = new CastPlayer(CastContext.getSharedInstance(this)); - castPlayer.setSessionAvailabilityListener(this); - } - - private void initializeMediaLibrarySession() { - mediaLibrarySession = new MediaLibrarySession.Builder(this, player, new LibrarySessionCallback()) - .setMediaItemFiller(new CustomMediaItemFiller()) - .setSessionActivity(PendingIntent.getActivity(getApplicationContext(), REQUEST_CODE, new Intent(getApplicationContext(), MainActivity.class), PendingIntent.FLAG_IMMUTABLE)) - .build(); - } - - private void setPlayer(Player oldPlayer, Player newPlayer) { - if (oldPlayer == newPlayer) return; - if (oldPlayer != null) oldPlayer.stop(); - - mediaLibrarySession.setPlayer(newPlayer); - } - - @Override - public void onCastSessionAvailable() { - setPlayer(player, castPlayer); - } - - @Override - public void onCastSessionUnavailable() { - setPlayer(castPlayer, player); - } - - private void releasePlayer() { - castPlayer.setSessionAvailabilityListener(null); - castPlayer.release(); - player.release(); - mediaLibrarySession.release(); - } - - private class LibrarySessionCallback implements MediaLibrarySession.MediaLibrarySessionCallback { - } - - private class CustomMediaItemFiller implements MediaSession.MediaItemFiller { - @Override - public MediaItem fillInLocalConfiguration(MediaSession session, MediaSession.ControllerInfo controller, MediaItem mediaItem) { - return mediaItem.buildUpon() - .setUri(mediaItem.mediaMetadata.mediaUri) - .setMediaMetadata(mediaItem.mediaMetadata) - .setMimeType(MimeTypes.BASE_TYPE_AUDIO) - .build(); - } - } - - private void initializePlayerListener() { - player.addListener(new Player.Listener() { - @Override - public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { - if (mediaItem == null) return; - - MediaManager.setLastPlayedTimestamp(mediaItem); - if (mediaItem.mediaMetadata.extras.getString("mediaType").equals(Media.MEDIA_TYPE_MUSIC)) - MediaManager.scrobble(mediaItem); - } - - @Override - public void onIsPlayingChanged(boolean isPlaying) { - if (isPlaying) { - MediaManager.setPlayingPausedTimestamp(player.getCurrentMediaItem(), player.getCurrentPosition()); - } - } - }); - } -} diff --git a/app/src/main/java/com/cappielloantonio/play/service/MediaService.kt b/app/src/main/java/com/cappielloantonio/play/service/MediaService.kt new file mode 100644 index 00000000..fb261178 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/play/service/MediaService.kt @@ -0,0 +1,280 @@ +package com.cappielloantonio.play.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.exoplayer.ExoPlayer +import androidx.media3.session.* +import androidx.media3.session.MediaSession.ControllerInfo +import com.cappielloantonio.play.App +import com.cappielloantonio.play.R +import com.cappielloantonio.play.model.Media +import com.cappielloantonio.play.repository.QueueRepository +import com.cappielloantonio.play.ui.activity.MainActivity +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 + + +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 (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 -> + 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: MediaSession.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) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setHandleAudioBecomingNoisy(true) + .setWakeMode(C.WAKE_MODE_NETWORK) + .build() + } + + private fun initializeCastPlayer() { + 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 + MediaManager.setLastPlayedTimestamp(mediaItem) + if (mediaItem.mediaMetadata.extras!!.getString("mediaType") == Media.MEDIA_TYPE_MUSIC) + MediaManager.scrobble(mediaItem) + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + if (isPlaying) { + MediaManager.setPlayingPausedTimestamp( + player.currentMediaItem, + player.currentPosition + ) + } + } + }) + } + + private fun setPlayer(oldPlayer: Player?, newPlayer: Player) { + if (oldPlayer === newPlayer) return + oldPlayer?.stop() + mediaLibrarySession.player = newPlayer + } + + private fun releasePlayer() { + castPlayer.setSessionAvailabilityListener(null) + 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. */ + } + + override fun onCastSessionAvailable() { + setPlayer(player, castPlayer) + } + + override fun onCastSessionUnavailable() { + setPlayer(castPlayer, player) + } +} \ No newline at end of file