From 5891ec800c6fcef0d943550cdf4cb35fe5259cb7 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Tue, 30 Sep 2025 15:41:58 -0700 Subject: [PATCH 1/3] chore: groundwork for heart rating --- .../com/cappielloantonio/tempo/model/SessionMediaItem.kt | 8 ++++++++ .../java/com/cappielloantonio/tempo/util/Constants.kt | 6 ++++++ .../com/cappielloantonio/tempo/util/MappingUtil.java | 9 +++++++++ 3 files changed, 23 insertions(+) diff --git a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt index 90d01f90..60d641ce 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/model/SessionMediaItem.kt @@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.model import android.net.Uri import android.os.Bundle import androidx.annotation.Keep +import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem.RequestMetadata import androidx.media3.common.MediaMetadata @@ -243,6 +244,13 @@ class SessionMediaItem() { .setAlbumTitle(album) .setArtist(artist) .setArtworkUri(artworkUri) + .setUserRating(HeartRating(starred != null)) + .setSupportedCommands( + listOf( + Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON, + Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF + ) + ) .setExtras(bundle) .setIsBrowsable(false) .setIsPlayable(true) diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt index da8862df..2752f1df 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -116,4 +116,10 @@ object Constants { const val HOME_SECTOR_RECENTLY_ADDED = "HOME_SECTOR_RECENTLY_ADDED" const val HOME_SECTOR_PINNED_PLAYLISTS = "HOME_SECTOR_PINNED_PLAYLISTS" const val HOME_SECTOR_SHARED = "HOME_SECTOR_SHARED" + + const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON = "android.media3.session.demo.SHUFFLE_ON" + const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = "android.media3.session.demo.SHUFFLE_OFF" + const val CUSTOM_COMMAND_TOGGLE_HEART_ON = "android.media3.session.demo.HEART_ON" + const val CUSTOM_COMMAND_TOGGLE_HEART_OFF = "android.media3.session.demo.HEART_OFF" + const val CUSTOM_COMMAND_TOGGLE_HEART_LOADING = "android.media3.session.demo.HEART_LOADING" } \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java index f8f15f07..e254d3c7 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java @@ -8,6 +8,7 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.HeartRating; import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.glide.CustomGlideRequest; @@ -16,6 +17,7 @@ import com.cappielloantonio.tempo.repository.DownloadRepository; import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.List; @@ -83,6 +85,13 @@ public class MappingUtil { .setAlbumTitle(media.getAlbum()) .setArtist(media.getArtist()) .setArtworkUri(artworkUri) + .setUserRating(new HeartRating(media.getStarred() != null)) + .setSupportedCommands( + ImmutableList.of( + Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON, + Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF + ) + ) .setExtras(bundle) .setIsBrowsable(false) .setIsPlayable(true) From a940af934c97531441ff4b993da3e2e6af91a6b5 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Wed, 1 Oct 2025 22:27:26 -0700 Subject: [PATCH 2/3] feat: notification-heart-rating --- .../cappielloantonio/tempo/util/Constants.kt | 3 + app/src/main/res/values/strings.xml | 3 + .../service/MediaLibraryServiceCallback.kt | 353 ++++++++++++++---- .../tempo/service/MediaService.kt | 6 - 4 files changed, 286 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt index 2752f1df..bd0cc26d 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Constants.kt @@ -122,4 +122,7 @@ object Constants { const val CUSTOM_COMMAND_TOGGLE_HEART_ON = "android.media3.session.demo.HEART_ON" const val CUSTOM_COMMAND_TOGGLE_HEART_OFF = "android.media3.session.demo.HEART_OFF" const val CUSTOM_COMMAND_TOGGLE_HEART_LOADING = "android.media3.session.demo.HEART_LOADING" + const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = "android.media3.session.demo.REPEAT_OFF" + const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = "android.media3.session.demo.REPEAT_ONE" + const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = "android.media3.session.demo.REPEAT_ALL" } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bd69ed38..37b0e501 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,6 +88,9 @@ Required http or https prefix required Downloads + Toggle Heart off + Toggle Heart on + Loading… Select two or more filters Filter Filter artists diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt index 099ae672..f9254974 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt @@ -2,21 +2,41 @@ package com.cappielloantonio.tempo.service import android.content.Context import android.os.Bundle +import android.util.Log import androidx.annotation.OptIn +import androidx.concurrent.futures.CallbackToFutureAdapter +import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata import androidx.media3.common.Player +import androidx.media3.common.Rating import androidx.media3.common.util.UnstableApi import androidx.media3.session.CommandButton import androidx.media3.session.LibraryResult +import androidx.media3.session.MediaConstants import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionError import androidx.media3.session.SessionResult +import com.cappielloantonio.tempo.App import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.repository.AutomotiveRepository +import com.cappielloantonio.tempo.subsonic.base.ApiResponse +import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_HEART_LOADING +import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_HEART_OFF +import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_HEART_ON +import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL +import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF +import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE +import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF +import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response open class MediaLibrarySessionCallback( context: Context, @@ -28,82 +48,253 @@ open class MediaLibrarySessionCallback( MediaBrowserTree.initialize(automotiveRepository) } - private val shuffleCommandButtons: List = listOf( - CommandButton.Builder() - .setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description)) - .setSessionCommand( - SessionCommand( - CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY - ) - ).setIconResId(R.drawable.exo_icon_shuffle_off).build(), + private val customCommandToggleShuffleModeOn = CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description)) + .setSessionCommand( + SessionCommand( + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY + ) + ).setIconResId(R.drawable.exo_icon_shuffle_off).build() - CommandButton.Builder() - .setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description)) - .setSessionCommand( - SessionCommand( - CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY - ) - ).setIconResId(R.drawable.exo_icon_shuffle_on).build() + private val customCommandToggleShuffleModeOff = CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description)) + .setSessionCommand( + SessionCommand( + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY + ) + ).setIconResId(R.drawable.exo_icon_shuffle_on).build() + + private val customCommandToggleRepeatModeOff = CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_repeat_off_description)) + .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY)) + .setIconResId(R.drawable.exo_icon_repeat_off) + .build() + + private val customCommandToggleRepeatModeOne = CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_repeat_one_description)) + .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY)) + .setIconResId(R.drawable.exo_icon_repeat_one) + .build() + + private val customCommandToggleRepeatModeAll = CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_repeat_all_description)) + .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY)) + .setIconResId(R.drawable.exo_icon_repeat_all) + .build() + + private val customCommandToggleHeartOn = CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_heart_on_description)) + .setSessionCommand( + SessionCommand( + CUSTOM_COMMAND_TOGGLE_HEART_ON, Bundle.EMPTY + ) + ) + .setIconResId(R.drawable.ic_favorite) + .build() + + private val customCommandToggleHeartOff = CommandButton.Builder() + .setDisplayName(context.getString(R.string.exo_controls_heart_off_description)) + .setSessionCommand( + SessionCommand(CUSTOM_COMMAND_TOGGLE_HEART_OFF, Bundle.EMPTY) + ) + .setIconResId(R.drawable.ic_favorites_outlined) + .build() + + // Fake Command while waiting for like update command + private val customCommandToggleHeartLoading = CommandButton.Builder() + .setDisplayName(context.getString(R.string.cast_expanded_controller_loading)) + .setSessionCommand( + SessionCommand(CUSTOM_COMMAND_TOGGLE_HEART_LOADING, Bundle.EMPTY) + ) + .setIconResId(R.drawable.ic_bookmark_sync) + .build() + + private val customLayoutCommandButtons = listOf( + customCommandToggleShuffleModeOn, + customCommandToggleShuffleModeOff, + customCommandToggleRepeatModeOff, + customCommandToggleRepeatModeOne, + customCommandToggleRepeatModeAll, + customCommandToggleHeartOn, + customCommandToggleHeartOff, + customCommandToggleHeartLoading, ) - private val repeatCommandButtons: List = listOf( - CommandButton.Builder() - .setDisplayName(context.getString(R.string.exo_controls_repeat_off_description)) - .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY)) - .setIconResId(R.drawable.exo_icon_repeat_off) - .build(), - CommandButton.Builder() - .setDisplayName(context.getString(R.string.exo_controls_repeat_one_description)) - .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY)) - .setIconResId(R.drawable.exo_icon_repeat_one) - .build(), - CommandButton.Builder() - .setDisplayName(context.getString(R.string.exo_controls_repeat_all_description)) - .setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY)) - .setIconResId(R.drawable.exo_icon_repeat_all) - .build() - ) - - private val customLayoutCommandButtons: List = - shuffleCommandButtons + repeatCommandButtons - @OptIn(UnstableApi::class) val mediaNotificationSessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon() .also { builder -> - (shuffleCommandButtons + repeatCommandButtons).forEach { commandButton -> + customLayoutCommandButtons.forEach { commandButton -> commandButton.sessionCommand?.let { builder.add(it) } } }.build() - fun buildCustomLayout(player: Player): ImmutableList { - val shuffle = shuffleCommandButtons[if (player.shuffleModeEnabled) 1 else 0] - val repeat = when (player.repeatMode) { - Player.REPEAT_MODE_ONE -> repeatCommandButtons[1] - Player.REPEAT_MODE_ALL -> repeatCommandButtons[2] - else -> repeatCommandButtons[0] - } - return ImmutableList.of(shuffle, repeat) - } - @OptIn(UnstableApi::class) override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): MediaSession.ConnectionResult { + session.player.addListener(object : Player.Listener { + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + updateMediaNotificationCustomLayout(session) + } + + override fun onRepeatModeChanged(repeatMode: Int) { + updateMediaNotificationCustomLayout(session) + } + + override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { + updateMediaNotificationCustomLayout(session) + } + + override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { + updateMediaNotificationCustomLayout(session) + } + }) + + // FIXME: I'm not sure this if is required anymore if (session.isMediaNotificationController(controller) || session.isAutomotiveController( controller ) || session.isAutoCompanionController(controller) ) { - val customLayout = buildCustomLayout(session.player) - return MediaSession.ConnectionResult.AcceptedResultBuilder(session) .setAvailableSessionCommands(mediaNotificationSessionCommands) - .setCustomLayout(customLayout).build() + .setCustomLayout(buildCustomLayout(session.player)) + .build() } return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build() } + // Update the mediaNotification after some changes + @OptIn(UnstableApi::class) + private fun updateMediaNotificationCustomLayout( + session: MediaSession, + isRatingPending: Boolean = false + ) { + session.setCustomLayout( + session.mediaNotificationControllerInfo!!, + buildCustomLayout(session.player, isRatingPending) + ) + } + + private fun buildCustomLayout(player: Player, isRatingPending: Boolean = false): ImmutableList { + val customLayout = mutableListOf() + + //TODO: create a user setting to decide which of the buttons to show on the mini player +// // Add shuffle button +// customLayout.add( +// if (player.shuffleModeEnabled) customCommandToggleShuffleModeOff else customCommandToggleShuffleModeOn +// ) + + // Add repeat button + val repeatButton = when (player.repeatMode) { + Player.REPEAT_MODE_ONE -> customCommandToggleRepeatModeOne + Player.REPEAT_MODE_ALL -> customCommandToggleRepeatModeAll + else -> customCommandToggleRepeatModeOff + } + + customLayout.add(repeatButton) + + // HEART_DEBUG logging + Log.d("HEART_DEBUG:", "Current media item: ${player.currentMediaItem}") + Log.d("HEART_DEBUG:", "User rating: ${player.mediaMetadata.userRating}") + Log.d("HEART_DEBUG:", "Is rating pending: $isRatingPending") + + // Add heart button if there's a current media item + if (player.currentMediaItem != null) { + if (isRatingPending) { + customLayout.add(customCommandToggleHeartLoading) + } else if ((player.mediaMetadata.userRating as HeartRating?)?.isHeart == true) { + customLayout.add(customCommandToggleHeartOff) + } else { + customLayout.add(customCommandToggleHeartOn) + } + } else { + Log.d("HEART_DEBUG:", "No current media item - skipping heart button") + } + + return ImmutableList.copyOf(customLayout) + } + + // Setting rating without a mediaId will set the currently listened mediaId + override fun onSetRating( + session: MediaSession, + controller: MediaSession.ControllerInfo, + rating: Rating + ): ListenableFuture { + return onSetRating(session, controller, session.player.currentMediaItem!!.mediaId, rating) + } + + override fun onSetRating( + session: MediaSession, + controller: MediaSession.ControllerInfo, + mediaId: String, + rating: Rating + ): ListenableFuture { + val isStaring = (rating as HeartRating).isHeart + + val networkCall = if (isStaring) + App.getSubsonicClientInstance(false) + .mediaAnnotationClient + .star(mediaId, null, null) + else + App.getSubsonicClientInstance(false) + .mediaAnnotationClient + .unstar(mediaId, null, null) + + return CallbackToFutureAdapter.getFuture { completer -> + networkCall.enqueue(object : Callback { + @OptIn(UnstableApi::class) + override fun onResponse( + call: Call, + response: Response + ) { + if (response.isSuccessful) { + + // Search if the media item in the player should be updated + for (i in 0 until session.player.mediaItemCount) { + val mediaItem = session.player.getMediaItemAt(i) + if (mediaItem.mediaId == mediaId) { + val newMetadata = mediaItem.mediaMetadata.buildUpon() + .setUserRating(HeartRating(isStaring)).build() + session.player.replaceMediaItem( + i, + mediaItem.buildUpon().setMediaMetadata(newMetadata).build() + ) + } + } + + updateMediaNotificationCustomLayout(session) + completer.set(SessionResult(SessionResult.RESULT_SUCCESS)) + } else { + updateMediaNotificationCustomLayout(session) + completer.set( + SessionResult( + SessionError( + response.code(), + response.message() + ) + ) + ) + } + } + + @OptIn(UnstableApi::class) + override fun onFailure(call: Call, t: Throwable) { + updateMediaNotificationCustomLayout(session) + completer.set( + SessionResult( + SessionError( + SessionError.ERROR_UNKNOWN, + "An error as occurred" + ) + ) + ) + } + }) + } + } + @OptIn(UnstableApi::class) override fun onCustomCommand( session: MediaSession, @@ -111,9 +302,23 @@ open class MediaLibrarySessionCallback( customCommand: SessionCommand, args: Bundle ): ListenableFuture { + + val mediaItemId = args.getString( + MediaConstants.EXTRA_KEY_MEDIA_ID, + session.player.currentMediaItem?.mediaId + ) + when (customCommand.customAction) { - CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true - CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> { + session.player.shuffleModeEnabled = true + updateMediaNotificationCustomLayout(session) + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> { + session.player.shuffleModeEnabled = false + updateMediaNotificationCustomLayout(session) + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> { @@ -123,16 +328,31 @@ open class MediaLibrarySessionCallback( else -> Player.REPEAT_MODE_OFF } session.player.repeatMode = nextMode + updateMediaNotificationCustomLayout(session) + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } - else -> return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) + CUSTOM_COMMAND_TOGGLE_HEART_ON, + CUSTOM_COMMAND_TOGGLE_HEART_OFF -> { + val currentRating = session.player.mediaMetadata.userRating as? HeartRating + val isCurrentlyLiked = currentRating?.isHeart ?: false + + val newLikedState = !isCurrentlyLiked + + updateMediaNotificationCustomLayout( + session, + isRatingPending = true // Show loading state + ) + return onSetRating(session, controller, HeartRating(newLikedState)) + } + else -> return Futures.immediateFuture( + SessionResult( + SessionError( + SessionError.ERROR_NOT_SUPPORTED, + customCommand.customAction + ) + ) + ) } - - session.setCustomLayout( - session.mediaNotificationControllerInfo!!, - buildCustomLayout(session.player) - ) - - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } override fun onGetLibraryRoot( @@ -186,17 +406,4 @@ open class MediaLibrarySessionCallback( ): ListenableFuture>> { return MediaBrowserTree.search(query) } - - 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" - private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF = - "android.media3.session.demo.REPEAT_OFF" - private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE = - "android.media3.session.demo.REPEAT_ONE" - private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL = - "android.media3.session.demo.REPEAT_ALL" - } } \ No newline at end of file diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt index a8b17ef6..963daa32 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -219,16 +219,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { Preferences.setShuffleModeEnabled(shuffleModeEnabled) - mediaLibrarySession.setCustomLayout( - librarySessionCallback.buildCustomLayout(player) - ) } override fun onRepeatModeChanged(repeatMode: Int) { Preferences.setRepeatMode(repeatMode) - mediaLibrarySession.setCustomLayout( - librarySessionCallback.buildCustomLayout(player) - ) } }) } From 9a64eeabe66d03e859656758d9824805758ef525 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Sun, 5 Oct 2025 12:59:24 -0700 Subject: [PATCH 3/3] feat: added preference to disable heart and show shuffle instead --- .../tempo/ui/fragment/SettingsFragment.java | 16 ++++++++ .../tempo/util/Preferences.kt | 11 +++++ app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/global_preferences.xml | 7 ++++ .../service/MediaLibraryServiceCallback.kt | 41 ++++++++----------- 5 files changed, 53 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index ef4f2134..0ee8d86f 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -117,6 +117,7 @@ public class SettingsFragment extends PreferenceFragmentCompat { actionDeleteDownloadStorage(); actionKeepScreenOn(); actionAutoDownloadLyrics(); + actionMiniPlayerHeart(); bindMediaService(); actionAppEqualizer(); @@ -358,6 +359,21 @@ public class SettingsFragment extends PreferenceFragmentCompat { }); } + private void actionMiniPlayerHeart() { + SwitchPreference preference = findPreference("mini_shuffle_button_visibility"); + if (preference == null) { + return; + } + + preference.setChecked(Preferences.showShuffleInsteadOfHeart()); + preference.setOnPreferenceChangeListener((pref, newValue) -> { + if (newValue instanceof Boolean) { + Preferences.setShuffleInsteadOfHeart((Boolean) newValue); + } + return true; + }); + } + private void actionAutoDownloadLyrics() { SwitchPreference preference = findPreference("auto_download_lyrics"); if (preference == null) { diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index 80276319..d3d81ee2 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -73,6 +73,7 @@ object Preferences { private const val LAST_INSTANT_MIX = "last_instant_mix" private const val EQUALIZER_ENABLED = "equalizer_enabled" private const val EQUALIZER_BAND_LEVELS = "equalizer_band_levels" + private const val MINI_SHUFFLE_BUTTON_VISIBILITY = "mini_shuffle_button_visibility" @JvmStatic fun getServer(): String? { @@ -359,6 +360,16 @@ object Preferences { ).apply() } + @JvmStatic + fun showShuffleInsteadOfHeart(): Boolean { + return App.getInstance().preferences.getBoolean(MINI_SHUFFLE_BUTTON_VISIBILITY, false) + } + + @JvmStatic + fun setShuffleInsteadOfHeart(enabled: Boolean) { + App.getInstance().preferences.edit().putBoolean(MINI_SHUFFLE_BUTTON_VISIBILITY, enabled).apply() + } + @JvmStatic fun showServerUnreachableDialog(): Boolean { return App.getInstance().preferences.getLong( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 70aa972f..2b7a4148 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -335,6 +335,8 @@ Sync timer If enabled, the user will have the ability to save their play queue and will have the ability to load state when opening the application. Sync play queue for this user [Not Fully Baked] + Show Shuffle button + If enabled, show the shuffle button, remove the heart in the mini player Show radio If enabled, show the radio section. Restart the app for it to take full effect. Auto download lyrics diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index f240a304..e8b5faeb 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -97,6 +97,13 @@ android:defaultValue="true" android:summary="@string/settings_music_directory_summary" android:key="music_directory_section_visibility" /> + + + diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt index f9254974..1815a815 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaLibraryServiceCallback.kt @@ -32,6 +32,7 @@ import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_REPEAT_MO import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF import com.cappielloantonio.tempo.util.Constants.CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON import com.google.common.collect.ImmutableList +import com.cappielloantonio.tempo.util.Preferences import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import retrofit2.Call @@ -180,11 +181,22 @@ open class MediaLibrarySessionCallback( private fun buildCustomLayout(player: Player, isRatingPending: Boolean = false): ImmutableList { val customLayout = mutableListOf() - //TODO: create a user setting to decide which of the buttons to show on the mini player -// // Add shuffle button -// customLayout.add( -// if (player.shuffleModeEnabled) customCommandToggleShuffleModeOff else customCommandToggleShuffleModeOn -// ) + val showShuffle = Preferences.showShuffleInsteadOfHeart() + + if (!showShuffle) { + if (player.currentMediaItem != null && !isRatingPending) { + // Heart button + if ((player.mediaMetadata.userRating as HeartRating?)?.isHeart == true) { + customLayout.add(customCommandToggleHeartOff) + } else { + customLayout.add(customCommandToggleHeartOn) + } + } + } else { + customLayout.add( + if (player.shuffleModeEnabled) customCommandToggleShuffleModeOff else customCommandToggleShuffleModeOn + ) + } // Add repeat button val repeatButton = when (player.repeatMode) { @@ -194,25 +206,6 @@ open class MediaLibrarySessionCallback( } customLayout.add(repeatButton) - - // HEART_DEBUG logging - Log.d("HEART_DEBUG:", "Current media item: ${player.currentMediaItem}") - Log.d("HEART_DEBUG:", "User rating: ${player.mediaMetadata.userRating}") - Log.d("HEART_DEBUG:", "Is rating pending: $isRatingPending") - - // Add heart button if there's a current media item - if (player.currentMediaItem != null) { - if (isRatingPending) { - customLayout.add(customCommandToggleHeartLoading) - } else if ((player.mediaMetadata.userRating as HeartRating?)?.isHeart == true) { - customLayout.add(customCommandToggleHeartOff) - } else { - customLayout.add(customCommandToggleHeartOn) - } - } else { - Log.d("HEART_DEBUG:", "No current media item - skipping heart button") - } - return ImmutableList.copyOf(customLayout) }