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()