From 0643b1bd1ccfad8a3297869a456df8b17adbd22c Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Sat, 20 May 2023 14:32:27 +0000 Subject: [PATCH] Refactor rating controls in Session --- .../ultrasonic/util/SimpleServiceBinder.java | 39 ------ .../ultrasonic/adapters/TrackViewHolder.kt | 13 +- .../ultrasonic/fragment/PlayerFragment.kt | 3 +- .../ultrasonic/fragment/SearchFragment.kt | 34 ++++- .../fragment/TrackCollectionFragment.kt | 86 ++++++------ .../playback/AutoMediaBrowserCallback.kt | 125 +++++++++++------- .../playback/CustomNotificationProvider.kt | 63 +-------- .../ultrasonic/playback/PlaybackService.kt | 34 ++++- .../ultrasonic/service/DownloadService.kt | 4 +- 9 files changed, 196 insertions(+), 205 deletions(-) delete mode 100644 ultrasonic/src/main/java/org/moire/ultrasonic/util/SimpleServiceBinder.java diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/SimpleServiceBinder.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/SimpleServiceBinder.java deleted file mode 100644 index 47360a1b..00000000 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/SimpleServiceBinder.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - This file is part of Subsonic. - - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - - Copyright 2009 (C) Sindre Mehus - */ -package org.moire.ultrasonic.util; - -import android.os.Binder; - -/** - * @author Sindre Mehus - */ -public class SimpleServiceBinder extends Binder -{ - private final S service; - - public SimpleServiceBinder(S service) - { - this.service = service; - } - - public S getService() - { - return service; - } -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt index 79b74069..313c6dd9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt @@ -11,6 +11,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.MutableLiveData import androidx.media3.common.HeartRating +import androidx.media3.common.StarRating import androidx.recyclerview.widget.RecyclerView import com.google.android.material.progressindicator.CircularProgressIndicator import io.reactivex.rxjava3.disposables.CompositeDisposable @@ -139,7 +140,17 @@ class TrackViewHolder(val view: View) : updateStatus(it.state, it.progress) } - // Timber.v("Setting song done") + // Listen for rating updates + rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe { + // Ignore updates which are not for the current song + if (it.id != song.id) return@subscribe + + if (it.rating is HeartRating) { + updateSingleStar(it.rating.isHeart) + } else if (it.rating is StarRating) { + updateFiveStars(it.rating.starRating.toInt()) + } + } } // This is called when the Holder is recycled and receives a new Song diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index bdc08eec..a0ca0c75 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -225,7 +225,7 @@ class PlayerFragment : fiveStar5ImageView = view.findViewById(R.id.song_five_star_5) } - @Suppress("LongMethod", "DEPRECATION") + @Suppress("LongMethod") @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { cancellationToken = CancellationToken() @@ -235,6 +235,7 @@ class PlayerFragment : val width: Int val height: Int + @Suppress("DEPRECATION") if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val bounds = windowManager.currentWindowMetrics.bounds width = bounds.width() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index e8a26e58..bd70f431 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -15,8 +15,11 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.appcompat.widget.SearchView +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.viewModelScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs @@ -55,8 +58,7 @@ import timber.log.Timber /** * Initiates a search on the media library and displays the results - * - * TODO: Implement the search field without using the deprecated OptionsMenu calls + * TODO: Switch to material3 class */ class SearchFragment : MultiListFragment(), KoinComponent { private var searchResult: SearchResult? = null @@ -80,7 +82,13 @@ class SearchFragment : MultiListFragment(), KoinComponent { super.onViewCreated(view, savedInstanceState) cancellationToken = CancellationToken() setTitle(this, R.string.search_title) - setHasOptionsMenu(true) + + // Register our options menu + (requireActivity() as MenuHost).addMenuProvider( + menuProvider, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) listModel.searchResult.observe( viewLifecycleOwner @@ -141,12 +149,24 @@ class SearchFragment : MultiListFragment(), KoinComponent { } /** - * This method creates the search bar above the recycler view + * This provide creates the search bar above the recycler view */ - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + private val menuProvider: MenuProvider = object : MenuProvider { + override fun onPrepareMenu(menu: Menu) { + setupOptionsMenu(menu) + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.search, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return true + } + } + fun setupOptionsMenu(menu: Menu) { val activity = activity ?: return val searchManager = activity.getSystemService(Context.SEARCH_SERVICE) as SearchManager - inflater.inflate(R.menu.search, menu) val searchItem = menu.findItem(R.id.search_item) searchView = searchItem.actionView as SearchView val searchableInfo = searchManager.getSearchableInfo(requireActivity().componentName) @@ -275,7 +295,7 @@ class SearchFragment : MultiListFragment(), KoinComponent { id = item.id, name = item.name, parentId = item.id, - isArtist = (item is Artist) + isArtist = false ) } else { SearchFragmentDirections.searchToAlbumsList( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 9e03df83..f771710f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -12,8 +12,11 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LiveData import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewModelScope @@ -114,7 +117,13 @@ open class TrackCollectionFragment( setupButtons(view) registerForContextMenu(listView!!) - setHasOptionsMenu(true) + + // Register our options menu + (requireActivity() as MenuHost).addMenuProvider( + menuProvider, + viewLifecycleOwner, + Lifecycle.State.RESUMED + ) // Create a View Manager viewManager = LinearLayoutManager(this.context) @@ -257,41 +266,39 @@ open class TrackCollectionFragment( } } - override fun onPrepareOptionsMenu(menu: Menu) { - super.onPrepareOptionsMenu(menu) - playAllButton = menu.findItem(R.id.select_album_play_all) + private val menuProvider: MenuProvider = object : MenuProvider { + override fun onPrepareMenu(menu: Menu) { + playAllButton = menu.findItem(R.id.select_album_play_all) - if (playAllButton != null) { - playAllButton!!.isVisible = playAllButtonVisible + if (playAllButton != null) { + playAllButton!!.isVisible = playAllButtonVisible + } + + shareButton = menu.findItem(R.id.menu_item_share) + + if (shareButton != null) { + shareButton!!.isVisible = shareButtonVisible + } } - shareButton = menu.findItem(R.id.menu_item_share) - - if (shareButton != null) { - shareButton!!.isVisible = shareButtonVisible - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.select_album, menu) - super.onCreateOptionsMenu(menu, inflater) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val itemId = item.itemId - if (itemId == R.id.select_album_play_all) { - playAll() - return true - } else if (itemId == R.id.menu_item_share) { - shareHandler.createShare( - this, getSelectedSongs(), - refreshListView, cancellationToken!!, - navArgs.id - ) - return true + override fun onCreateMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.select_album, menu) } - return false + override fun onMenuItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.select_album_play_all) { + playAll() + return true + } else if (item.itemId == R.id.menu_item_share) { + shareHandler.createShare( + this@TrackCollectionFragment, getSelectedSongs(), + refreshListView, cancellationToken!!, + navArgs.id + ) + return true + } + return false + } } override fun onDestroyView() { @@ -379,20 +386,17 @@ open class TrackCollectionFragment( private fun selectAllOrNone() { val someUnselected = viewAdapter.selectedSet.size < childCount - - selectAll(someUnselected, true) + selectAll(someUnselected) } - private fun selectAll(selected: Boolean, toast: Boolean) { + private fun selectAll(selected: Boolean) { var selectedCount = viewAdapter.selectedSet.size * -1 selectedCount += viewAdapter.setSelectionStatusOfAll(selected) // Display toast: N tracks selected - if (toast) { - val toastResId = R.string.select_album_n_selected - Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0))) - } + val toastResId = R.string.select_album_n_selected + Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0))) } @Synchronized @@ -575,7 +579,7 @@ open class TrackCollectionFragment( setTitle(R.string.main_videos) listModel.getVideos(refresh2) } else if (id == null || getRandomTracks) { - // There seems to be a bug in ViewPager when resuming the Actitivy that subfragments + // There seems to be a bug in ViewPager when resuming the Activity that sub-fragments // arguments are empty. If we have no id, just show some random tracks setTitle(R.string.main_songs_random) listModel.getRandom(size, append) @@ -636,10 +640,6 @@ open class TrackCollectionFragment( R.id.song_menu_download -> { downloadBackground(false, songs) } - R.id.select_album_play_all -> { - // TODO: Why is this being handled here?! - playAll() - } R.id.song_menu_share -> { if (item is Track) { shareHandler.createShare( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt index 456f23ef..3abb29df 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/AutoMediaBrowserCallback.kt @@ -9,8 +9,6 @@ package org.moire.ultrasonic.playback import android.annotation.SuppressLint import android.os.Bundle -import android.widget.Toast -import android.widget.Toast.LENGTH_SHORT import androidx.media3.common.HeartRating import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata @@ -20,6 +18,7 @@ import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES import androidx.media3.common.Rating +import androidx.media3.session.CommandButton import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession @@ -27,7 +26,6 @@ import androidx.media3.session.SessionCommand import androidx.media3.session.SessionResult import androidx.media3.session.SessionResult.RESULT_SUCCESS import com.google.common.collect.ImmutableList -import com.google.common.util.concurrent.FutureCallback import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope @@ -49,7 +47,6 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.MediaPlayerManager import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.RatingManager -import org.moire.ultrasonic.util.MainThreadExecutor import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.buildMediaItem import org.moire.ultrasonic.util.toMediaItem @@ -92,7 +89,6 @@ private const val DISPLAY_LIMIT = 100 private const val SEARCH_LIMIT = 10 // List of available custom SessionCommands -const val SESSION_CUSTOM_SET_RATING = "SESSION_CUSTOM_SET_RATING" const val PLAY_COMMAND = "play " /** @@ -119,6 +115,24 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : private val isOffline get() = ActiveServerProvider.isOffline() private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId + private var customCommands: List + internal var customLayout = ImmutableList.of() + + init { + customCommands = + listOf( + // This button is used for an unstarred track, and its action will star the track + getHeartCommandButton( + SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON, Bundle.EMPTY) + ), + // This button is used for an starred track, and its action will unstar the track + getHeartCommandButton( + SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF, Bundle.EMPTY) + ) + ) + customLayout = ImmutableList.of(customCommands[0]) + } + /** * Called when a {@link MediaBrowser} requests the root {@link MediaItem} by {@link * MediaBrowser#getLibraryRoot(LibraryParams)}. @@ -176,11 +190,10 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : val connectionResult = super.onConnect(session, controller) val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() - /* - * TODO: Currently we need to create a custom session command, see https://github.com/androidx/media/issues/107 - * When this issue is fixed we should be able to remove this method again - */ - availableSessionCommands.add(SessionCommand(SESSION_CUSTOM_SET_RATING, Bundle())) + for (commandButton in customCommands) { + // Add custom command to available session commands. + commandButton.sessionCommand?.let { availableSessionCommands.add(it) } + } return MediaSession.ConnectionResult.accept( availableSessionCommands.build(), @@ -188,6 +201,28 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : ) } + override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { + if (!customLayout.isEmpty() && controller.controllerVersion != 0) { + // Let Media3 controller (for instance the MediaNotificationProvider) + // know about the custom layout right after it connected. + session.setCustomLayout(customLayout) + } + } + + private fun getHeartCommandButton(sessionCommand: SessionCommand): CommandButton { + val willHeart = + (sessionCommand.customAction == PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON) + return CommandButton.Builder() + .setDisplayName("Love") + .setIconResId( + if (willHeart) R.drawable.ic_star_hollow + else R.drawable.ic_star_full + ) + .setSessionCommand(sessionCommand) + .setEnabled(true) + .build() + } + override fun onGetItem( session: MediaLibraryService.MediaLibrarySession, browser: MediaSession.ControllerInfo, @@ -201,12 +236,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : // Create LRU Cache of MediaItems, fill it in the other calls // and retrieve it here. - if (mediaItem != null) { - return Futures.immediateFuture( + return if (mediaItem != null) { + Futures.immediateFuture( LibraryResult.ofItem(mediaItem, null) ) } else { - return Futures.immediateFuture( + Futures.immediateFuture( LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE) ) } @@ -234,40 +269,13 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : var customCommandFuture: ListenableFuture? = null when (customCommand.customAction) { - SESSION_CUSTOM_SET_RATING -> { - /* - * It is currently not possible to edit a MediaItem after creation so the isRated value - * is stored in the track.starred value - * See https://github.com/androidx/media/issues/33 - */ - val track = mediaPlayerManager.currentMediaItem?.toTrack() - if (track != null) { - customCommandFuture = onSetRating( - session, - controller, - HeartRating(!track.starred) - ) - Futures.addCallback( - customCommandFuture, - object : FutureCallback { - override fun onSuccess(result: SessionResult) { - track.starred = !track.starred - // This needs to be called on the main Thread - // TODO: This is a looping reference - libraryService.onUpdateNotification(session) - } - - override fun onFailure(t: Throwable) { - Toast.makeText( - mediaPlayerManager.context, - "There was an error updating the rating", - LENGTH_SHORT - ).show() - } - }, - MainThreadExecutor() - ) - } + PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON -> { + customCommandFuture = onSetRating(session, controller, HeartRating(true)) + updateCustomHeartButton(session, true) + } + PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF -> { + customCommandFuture = onSetRating(session, controller, HeartRating(false)) + updateCustomHeartButton(session, false) } else -> { Timber.d( @@ -281,19 +289,21 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : return customCommandFuture return super.onCustomCommand(session, controller, customCommand, args) } - override fun onSetRating( session: MediaSession, controller: MediaSession.ControllerInfo, rating: Rating ): ListenableFuture { - if (session.player.currentMediaItem != null) + val mediaItem = session.player.currentMediaItem + if (mediaItem != null) { + mediaItem.toTrack().starred = (rating as HeartRating).isHeart return onSetRating( session, controller, - session.player.currentMediaItem!!.mediaId, + mediaItem.mediaId, rating ) + } return super.onSetRating(session, controller, rating) } @@ -303,6 +313,9 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : mediaId: String, rating: Rating ): ListenableFuture { + // TODO: Through this methods it is possible to set a rating on an arbitrary MediaItem. + // Right now the ratings are submitted, yet the underlying track is only updated when + // coming from the other onSetRating(session, controller, rating) return serviceScope.future { Timber.i(controller.packageName) // This function even though its declared in AutoMediaBrowserCallback.kt is @@ -324,7 +337,6 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : * and thereby customarily it is required to rebuild it.. * See also: https://stackoverflow.com/questions/70096715/adding-mediaitem-when-using-the-media3-library-caused-an-error */ - override fun onAddMediaItems( mediaSession: MediaSession, controller: MediaSession.ControllerInfo, @@ -1276,4 +1288,15 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) : null } } + + fun updateCustomHeartButton( + session: MediaSession, + isHeart: Boolean + ) { + val command = if (isHeart) customCommands[1] else customCommands[0] + // Change the custom layout to contain the right heart button + customLayout = ImmutableList.of(command) + // Send the updated custom layout to controllers. + session.setCustomLayout(customLayout) + } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt index 0f283267..50fa4c4f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/CustomNotificationProvider.kt @@ -7,79 +7,22 @@ package org.moire.ultrasonic.playback import android.content.Context -import androidx.core.app.NotificationCompat -import androidx.media3.common.HeartRating import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.session.CommandButton import androidx.media3.session.DefaultMediaNotificationProvider -import androidx.media3.session.MediaNotification import androidx.media3.session.MediaSession -import androidx.media3.session.SessionCommand import com.google.common.collect.ImmutableList import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.moire.ultrasonic.R -import org.moire.ultrasonic.service.MediaPlayerManager -import org.moire.ultrasonic.util.toTrack @UnstableApi class CustomNotificationProvider(ctx: Context) : DefaultMediaNotificationProvider(ctx), KoinComponent { - /* - * It is currently not possible to edit a MediaItem after creation so the isRated value - * is stored in the track.starred value. See https://github.com/androidx/media/issues/33 - * TODO: Once the bug is fixed remove this circular reference! - */ - private val mediaPlayerManager by inject() - - override fun addNotificationActions( - mediaSession: MediaSession, - mediaButtons: ImmutableList, - builder: NotificationCompat.Builder, - actionFactory: MediaNotification.ActionFactory - ): IntArray { - val tmp: MutableList = mutableListOf() - /* - * TODO: - * It is currently not possible to edit a MediaItem after creation so the isRated value - * is stored in the track.starred value - * See https://github.com/androidx/media/issues/33 - */ - val rating = mediaPlayerManager.currentMediaItem?.toTrack()?.starred?.let { - HeartRating( - it - ) - } - if (rating is HeartRating) { - tmp.add( - CommandButton.Builder() - .setDisplayName("Love") - .setIconResId( - if (rating.isHeart) R.drawable.ic_star_full - else R.drawable.ic_star_hollow - ) - .setSessionCommand( - SessionCommand( - SESSION_CUSTOM_SET_RATING, - HeartRating(rating.isHeart).toBundle() - ) - ) - .setExtras(HeartRating(rating.isHeart).toBundle()) - .setEnabled(true) - .build() - ) - } - return super.addNotificationActions( - mediaSession, - ImmutableList.copyOf((mediaButtons + tmp)), - builder, - actionFactory - ) - } - + // By default the skip buttons are not shown in compact view. + // We add the COMMAND_KEY_COMPACT_VIEW_INDEX to show them + // See also: https://github.com/androidx/media/issues/410 override fun getMediaButtons( session: MediaSession, playerCommands: Player.Commands, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt index 6d0f0db6..6dd656a3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/playback/PlaybackService.kt @@ -68,7 +68,7 @@ class PlaybackService : private var equalizer: EqualizerController? = null private val activeServerProvider: ActiveServerProvider by inject() - private lateinit var librarySessionCallback: MediaLibrarySession.Callback + private lateinit var librarySessionCallback: AutoMediaBrowserCallback private var rxBusSubscription = CompositeDisposable() @@ -132,6 +132,13 @@ class PlaybackService : setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext())) + // TODO: Remove minor code duplication with updateBackend() + val desiredBackend = if (activeServerProvider.getActiveServer().jukeboxByDefault) { + MediaPlayerManager.PlayerBackend.JUKEBOX + } else { + MediaPlayerManager.PlayerBackend.LOCAL + } + player = if (activeServerProvider.getActiveServer().jukeboxByDefault) { Timber.i("Jukebox enabled by default") getJukeboxPlayer() @@ -139,6 +146,8 @@ class PlaybackService : getLocalPlayer() } + actualBackend = desiredBackend + // Create browser interface librarySessionCallback = AutoMediaBrowserCallback(this) @@ -148,6 +157,11 @@ class PlaybackService : .setBitmapLoader(ArtworkBitmapLoader()) .build() + if (!librarySessionCallback.customLayout.isEmpty()) { + // Send custom layout to legacy session. + mediaLibrarySession.setCustomLayout(librarySessionCallback.customLayout) + } + // Set a listener to update the API client when the active server has changed rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { // Set the player wake mode @@ -209,6 +223,7 @@ class PlaybackService : player.addListener(listener) mediaLibrarySession.player = player + actualBackend = newBackend } @@ -281,7 +296,14 @@ class PlaybackService : } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { - updateWidgetTrack(mediaItem?.toTrack()) + // Since we cannot update the metadata of the media item after creation, + // we cannot set change the rating on it + // Therefore the track must be our source of truth + val track = mediaItem?.toTrack() + if (track != null) { + updateCustomHeartButton(track.starred) + } + updateWidgetTrack(track) cacheNextSongs() } @@ -291,6 +313,10 @@ class PlaybackService : } } + private fun updateCustomHeartButton(isHeart: Boolean) { + librarySessionCallback.updateCustomHeartButton(mediaLibrarySession, isHeart) + } + private fun cacheNextSongs() { if (actualBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) return Timber.d("PlaybackService caching the next songs") @@ -394,6 +420,10 @@ class PlaybackService : private const val NOTIFICATION_CHANNEL_ID = "org.moire.ultrasonic.error" private const val NOTIFICATION_CHANNEL_NAME = "Ultrasonic error messages" + const val CUSTOM_COMMAND_TOGGLE_HEART_ON = + "org.moire.ultrasonic.HEART_ON" + const val CUSTOM_COMMAND_TOGGLE_HEART_OFF = + "org.moire.ultrasonic.HEART_OFF" private const val NOTIFICATION_ID = 3009 } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index 946ce471..808c3bbe 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -11,6 +11,7 @@ import android.app.Notification import android.app.Service import android.content.Intent import android.net.wifi.WifiManager +import android.os.Binder import android.os.Build import android.os.Handler import android.os.IBinder @@ -39,7 +40,6 @@ import org.moire.ultrasonic.util.FileUtil.getCompleteFile import org.moire.ultrasonic.util.FileUtil.getPartialFile import org.moire.ultrasonic.util.FileUtil.getPinnedFile import org.moire.ultrasonic.util.Settings -import org.moire.ultrasonic.util.SimpleServiceBinder import org.moire.ultrasonic.util.Storage import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.stopForegroundRemoveNotification @@ -452,3 +452,5 @@ class DownloadService : Service(), KoinComponent { } } } + +class SimpleServiceBinder(val service: S) : Binder()