Add song rating to notification

This commit is contained in:
Maxence G 2022-07-02 01:27:12 +02:00
parent ac489ae8b9
commit 9014b47b74
No known key found for this signature in database
GPG Key ID: DC1FD9409E3FE284
5 changed files with 209 additions and 36 deletions

View File

@ -153,6 +153,8 @@ class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable
star.setImageDrawable(imageHelper.starHollowDrawable)
song.starred = false
}
// Should this be done here ?
Thread {
val musicService = MusicServiceFactory.getMusicService()
try {

View File

@ -33,17 +33,22 @@ import android.widget.LinearLayout
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import android.widget.TextView
import android.widget.Toast
import android.widget.ViewFlipper
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.media3.common.HeartRating
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.session.SessionResult
import androidx.navigation.Navigation
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import io.reactivex.rxjava3.disposables.CompositeDisposable
import java.text.DateFormat
import java.text.SimpleDateFormat
@ -748,25 +753,28 @@ class PlayerFragment :
val isStarred = currentSong!!.starred
val id = currentSong!!.id
if (isStarred) {
starMenuItem.icon = hollowStar
currentSong!!.starred = false
} else {
starMenuItem.icon = fullStar
currentSong!!.starred = true
}
Thread {
val musicService = getMusicService()
try {
if (isStarred) {
musicService.unstar(id, null, null)
} else {
musicService.star(id, null, null)
mediaPlayerController.controller?.setRating(
id,
HeartRating(!isStarred)
)?.let {
Futures.addCallback(it, object : FutureCallback<SessionResult> {
override fun onSuccess(result: SessionResult?) {
if (isStarred) {
starMenuItem.icon = hollowStar
currentSong!!.starred = false
} else {
starMenuItem.icon = fullStar
currentSong!!.starred = true
}
}
} catch (all: Exception) {
Timber.e(all)
}
}.start()
override fun onFailure(t: Throwable) {
Toast.makeText(context, "SetRating failed", Toast.LENGTH_SHORT).show()
}
}, this.executorService)
}
return true
}
R.id.menu_item_bookmark_set -> {

View File

@ -9,6 +9,7 @@ package org.moire.ultrasonic.playback
import android.net.Uri
import android.os.Bundle
import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS
@ -18,9 +19,15 @@ import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
import androidx.media3.common.Player
import androidx.media3.common.Rating
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import androidx.media3.session.SessionResult.RESULT_ERROR_BAD_VALUE
import androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN
import androidx.media3.session.SessionResult.RESULT_SUCCESS
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
@ -150,6 +157,22 @@ class AutoMediaBrowserCallback(var player: Player) :
)
}
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
// TODO: Make a Const value list of available custom SessionCommands
availableSessionCommands.add(SessionCommand("COMMAND_CODE_SESSION_SET_RATING", Bundle()))
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
}
override fun onGetItem(
session: MediaLibraryService.MediaLibrarySession,
browser: MediaSession.ControllerInfo,
@ -177,6 +200,87 @@ class AutoMediaBrowserCallback(var player: Player) :
return onLoadChildren(parentId)
}
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
/*
* It is currently not possible to edit a MediaItem after creation so the isRated value
* is stored in the track.starred value
*/
val rating = mediaPlayerController.currentPlayingLegacy?.track?.starred?.let {
HeartRating(
it
)
}
if (rating is HeartRating) {
return when (customCommand.customAction) {
"COMMAND_CODE_SESSION_SET_RATING" -> {
onSetRating(
session,
controller,
HeartRating(!rating.isHeart)
)
}
else -> {
Timber.d(
"CustomCommand not recognized %s with extra %s",
customCommand.customAction,
customCommand.customExtras.toString()
)
super.onCustomCommand(session, controller, customCommand, args)
}
}
}
return super.onCustomCommand(session, controller, customCommand, args)
}
override fun onSetRating(
session: MediaSession,
controller: MediaSession.ControllerInfo,
rating: Rating
): ListenableFuture<SessionResult> {
if (session.player.currentMediaItem != null)
return onSetRating(
session,
controller,
session.player.currentMediaItem!!.mediaId,
rating
)
return super.onSetRating(session, controller, rating)
}
override fun onSetRating(
session: MediaSession,
controller: MediaSession.ControllerInfo,
mediaId: String,
rating: Rating
): ListenableFuture<SessionResult> {
return serviceScope.future {
if (rating is HeartRating) {
val musicService = MusicServiceFactory.getMusicService()
try {
if (rating.isHeart) {
musicService.star(mediaId, null, null)
} else {
musicService.unstar(mediaId, null, null)
}
} catch (all: Exception) {
Timber.e(all)
// TODO: Better handle exception
return@future SessionResult(RESULT_ERROR_UNKNOWN)
}
mediaPlayerController.currentPlayingLegacy?.track?.starred = rating.isHeart
return@future SessionResult(RESULT_SUCCESS)
}
return@future SessionResult(RESULT_ERROR_BAD_VALUE)
}
}
/*
* For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
* and thereby customarily it is required to rebuild it..
@ -1076,6 +1180,7 @@ class AutoMediaBrowserCallback(var player: Player) :
album = track.album,
artist = track.artist,
genre = track.genre,
starred = track.starred
)
}
@ -1090,6 +1195,7 @@ class AutoMediaBrowserCallback(var player: Player) :
genre: String? = null,
sourceUri: Uri? = null,
imageUri: Uri? = null,
starred: Boolean = false
): MediaItem {
val metadata =
MediaMetadata.Builder()
@ -1097,6 +1203,7 @@ class AutoMediaBrowserCallback(var player: Player) :
.setTitle(title)
.setArtist(artist)
.setGenre(genre)
.setUserRating(HeartRating(starred))
.setFolderType(folderType)
.setIsPlayable(isPlayable)
.setArtworkUri(imageUri)

View File

@ -9,15 +9,28 @@ 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 org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.service.MediaPlayerController
@UnstableApi
class MediaNotificationProvider(context: Context) : DefaultMediaNotificationProvider(context) {
class MediaNotificationProvider(context: Context) :
DefaultMediaNotificationProvider(context), KoinComponent {
/*
* It is currently not possible to edit a MediaItem after creation so the isRated value
* is stored in the track.starred value
*/
private val mediaPlayerController by inject<MediaPlayerController>()
override fun addNotificationActions(
mediaSession: MediaSession,
@ -25,7 +38,34 @@ class MediaNotificationProvider(context: Context) : DefaultMediaNotificationProv
builder: NotificationCompat.Builder,
actionFactory: MediaNotification.ActionFactory
): IntArray {
return super.addNotificationActions(mediaSession, mediaButtons, builder, actionFactory)
val tmp: MutableList<CommandButton> = mutableListOf()
val rating = mediaPlayerController.currentPlayingLegacy?.track?.starred?.let {
HeartRating(
it
)
}
if (rating is HeartRating) {
tmp.add(
CommandButton.Builder()
.setDisplayName("Love")
.setIconResId(if (rating.isHeart) R.drawable.ic_star_full_dark else R.drawable.ic_star_hollow_dark)
.setSessionCommand(
SessionCommand(
"COMMAND_CODE_SESSION_SET_RATING",
HeartRating(rating.isHeart).toBundle()
)
)
.setExtras(HeartRating(rating.isHeart).toBundle())
.setEnabled(true)
.build()
)
}
return super.addNotificationActions(
mediaSession,
mediaButtons + tmp,
builder,
actionFactory
)
}
override fun getMediaButtons(

View File

@ -9,13 +9,20 @@ package org.moire.ultrasonic.service
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.core.net.toUri
import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.RequestMetadata
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.session.MediaController
import androidx.media3.session.SessionResult
import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.MoreExecutors
import io.reactivex.rxjava3.disposables.CompositeDisposable
import kotlinx.coroutines.CoroutineScope
@ -579,23 +586,31 @@ class MediaPlayerController(
if (legacyPlaylistManager.currentPlaying == null) return
val song = legacyPlaylistManager.currentPlaying!!.track
Thread {
val musicService = getMusicService()
try {
if (song.starred) {
musicService.unstar(song.id, null, null)
} else {
musicService.star(song.id, null, null)
}
} catch (all: Exception) {
Timber.e(all)
}
}.start()
fun updateStarred() {
// Trigger an update
// TODO Update Metadata of MediaItem...
// localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying)
song.starred = !song.starred
}
// Trigger an update
// TODO Update Metadata of MediaItem...
// localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying)
song.starred = !song.starred
controller?.setRating(
song.id,
HeartRating(!song.starred)
).let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && it != null) {
Futures.addCallback(it, object : FutureCallback<SessionResult> {
override fun onSuccess(result: SessionResult?) {
updateStarred()
}
override fun onFailure(t: Throwable) {
TODO("Not yet implemented")
}
}, context.mainExecutor)
} else {
updateStarred()
}
}
}
@Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
@ -668,6 +683,7 @@ fun Track.toMediaItem(): MediaItem {
.setArtist(artist)
.setAlbumTitle(album)
.setAlbumArtist(artist)
.setUserRating(HeartRating(starred))
.build()
val mediaItem = MediaItem.Builder()