fix lint and session access from the notification star review

- opt in to the Media3 unstable API on the service and answer
  unsupported custom commands with SessionError.ERROR_NOT_SUPPORTED
- add the star command on top of the trust-aware connection defaults
  instead of granting untrusted controllers the full command set
- number tracks within their disc, not across the whole album
- leave the star button to the library flow so a failed write cannot
  leave the notification flipped
- omit the leading cell entirely when a row has no art and no track
This commit is contained in:
austinried
2026-10-04 00:30:02 +00:00
parent 060cbd2a91
commit f037e595ed
3 changed files with 38 additions and 18 deletions
@@ -22,6 +22,7 @@ import androidx.media3.session.CommandButton
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionError
import androidx.media3.session.SessionResult
import coil3.SingletonImageLoader
import com.google.common.util.concurrent.Futures
@@ -39,6 +40,7 @@ import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
import org.koin.core.context.GlobalContext
@OptIn(UnstableApi::class)
class PlaybackService : MediaSessionService() {
private var session: MediaSession? = null
private var artworkLoader: CoverArtBitmapLoader? = null
@@ -55,15 +57,17 @@ class PlaybackService : MediaSessionService() {
override fun onConnect(
session: MediaSession,
controller: MediaSession.ControllerInfo,
): MediaSession.ConnectionResult =
MediaSession.ConnectionResult
.AcceptedResultBuilder(session)
): MediaSession.ConnectionResult {
val base = MediaSession.ConnectionResult.AcceptedResultBuilder(session, controller).build()
return MediaSession.ConnectionResult
.AcceptedResultBuilder(session, controller)
.setAvailableSessionCommands(
MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS
base.availableSessionCommands
.buildUpon()
.add(STAR_COMMAND)
.build(),
).build()
}
override fun onCustomCommand(
session: MediaSession,
@@ -72,7 +76,7 @@ class PlaybackService : MediaSessionService() {
args: Bundle,
): ListenableFuture<SessionResult> {
if (customCommand.customAction != STAR_COMMAND.customAction) {
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED))
return Futures.immediateFuture(SessionResult(SessionError.ERROR_NOT_SUPPORTED))
}
toggleStar()
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
@@ -163,8 +167,7 @@ class PlaybackService : MediaSessionService() {
private fun toggleStar() {
val songId = currentSongId ?: return
val library = library ?: return
starred = !starred
library.star(StarType.Song, songId, starred)
library.star(StarType.Song, songId, !starred)
}
private fun starButton(isStarred: Boolean): CommandButton =
@@ -190,7 +193,6 @@ class PlaybackService : MediaSessionService() {
}.build()
override fun onDestroy() {
starJob?.cancel()
scope.cancel()
artworkLoader?.shutdown()
artworkLoader = null
@@ -132,6 +132,16 @@ fun AlbumDetailScreen(
}
}
}
val discOrdinals =
remember(songs) {
val counters = mutableMapOf<Long, Long>()
songs.associate { song ->
val disc = song.disc ?: 1L
val ordinal = (counters[disc] ?: 0L) + 1
counters[disc] = ordinal
song.id to ordinal
}
}
HeroDetailScaffold(
artwork = artwork,
title = album?.name.orEmpty(),
@@ -171,7 +181,7 @@ fun AlbumDetailScreen(
SongRow(
song = song,
isPlaying = song.id == playingSongId,
trackNumber = song.track ?: (index + 1).toLong(),
trackNumber = song.track ?: discOrdinals[song.id],
durationSeconds = song.duration,
download = downloads[song.id],
modifier =
@@ -57,16 +57,24 @@ fun SongRow(
ListItem(
modifier = modifier,
leadingContent =
if (coverArt != null) {
{
CoverArtCell(
ref = coverArt(coverArtId, true),
name = song.album ?: song.title,
isPlaying = isPlaying,
)
when {
coverArt != null -> {
{
CoverArtCell(
ref = coverArt(coverArtId, true),
name = song.album ?: song.title,
isPlaying = isPlaying,
)
}
}
trackNumber != null || isPlaying -> {
{ TrackCell(trackNumber, isPlaying) }
}
else -> {
null
}
} else {
{ TrackCell(trackNumber, isPlaying) }
},
trailingContent = {
Row(