From f037e595ed36167015df6b26fd525ede66c88f2a Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sun, 27 Sep 2026 08:36:22 +0000 Subject: [PATCH] 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 --- .../com/subtracks/playback/PlaybackService.kt | 18 +++++++------ .../subtracks/ui/library/AlbumDetailScreen.kt | 12 ++++++++- .../com/subtracks/ui/library/SongsContent.kt | 26 ++++++++++++------- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt b/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt index 5c612555..d2dd17ff 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlaybackService.kt @@ -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 { 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 diff --git a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt index ac1970ab..78a45178 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/AlbumDetailScreen.kt @@ -132,6 +132,16 @@ fun AlbumDetailScreen( } } } + val discOrdinals = + remember(songs) { + val counters = mutableMapOf() + 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 = diff --git a/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt b/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt index 21ecb6ef..07acb4a6 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/SongsContent.kt @@ -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(