diff --git a/AGENTS.md b/AGENTS.md index 5a22163..a1ff817 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -375,11 +375,11 @@ it. It is never published, so it has to be asked for. Upstream would close this with two lines — `def play(self, pos=None)` in `player/coordinator.py` and the MPD backend. Coil needs no change when it lands: the ladder's first rung starts succeeding. -## The media session, and the four things media3 will not tell you +## The media session, and the five things media3 will not tell you `PhonieboxPlayer` is a `SimpleBasePlayer` whose playback happens on the box, and `feature-media`'s -whole job is translating between the two. Four of media3's contracts are silent when broken — nothing -logs, nothing throws, a control simply does nothing — and all four were broken at some point: +whole job is translating between the two. Five of media3's contracts are silent when broken — nothing +logs, nothing throws, a control simply does nothing — and all five were broken at some point: - **A session the service was never handed shows no notification.** `MediaNotificationManager.updateNotification` asks `isSessionAdded` before anything else, and media3 @@ -401,6 +401,15 @@ logs, nothing throws, a control simply does nothing — and all four were broken it was before the command was sent. `PhonieboxPlayer.send` therefore stays pending until the box has been told and, where the outcome is published, until it says so — bounded, because `invalidateState` is ignored while anything is pending. +- **A null metadata title is not an empty line.** `DefaultMediaNotificationProvider` reads + `displayTitle ?: title` and posts the notification with no content title at all when both are + absent, and the platform fills that hole with its own " is running" — which is what the lock + screen showed for every untagged rip, artist and cover art beside it perfectly correct. It reads + as *stale* metadata rather than missing metadata, because within one untagged album the metadata + genuinely never changes: same null title, same albumartist, same folder cover, so + `SimpleBasePlayer` has nothing to report and the same wrong line stays up track after track. The + playing item's title therefore goes through `PlayerStatus.displayTitle`, which falls back to the + file name the way `QueueParser` and `LibraryTrack.displayTitle` already do. - **`mediaItemIndex` is a timeline index, not a queue position**, and `C.INDEX_UNSET` means "media3 resolved this to nothing". Both are read in one place, `seekIntentFor`, whose KDoc carries the reasoning; the short version is that a fallback timeline's only index means "the playing song", so diff --git a/CHANGELOG.md b/CHANGELOG.md index 635edde..93ecec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,16 @@ automatically from the `## [x.y.z]` heading matching `versionName` in `app/build ## [Unreleased] +## [1.2.1] - 2026-08-27 + +### Fixed +- **The lock screen names the track again.** For a file with no title tag — which is most of a + ripped-CD library — the controls in the shade and on the lock screen read "Coil is running" + instead of the track, and kept reading it as the box moved through the album, even though the + artist and the cover beside it were right all along. An untagged track is now named by its file + name there, the same way it already was in the queue and in the library, and the player screen + and the mini player name it that way too instead of saying "Unknown title" + ## [1.2.0] - 2026-08-19 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c1208f0..e16a4ce 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "app.coilforphoniebox" minSdk = 26 targetSdk = 36 - versionCode = 8 - versionName = "1.2.0" + versionCode = 9 + versionName = "1.2.1" } androidResources { diff --git a/app/src/main/kotlin/app/coilforphoniebox/ui/components/MiniPlayer.kt b/app/src/main/kotlin/app/coilforphoniebox/ui/components/MiniPlayer.kt index b57a862..163f60c 100644 --- a/app/src/main/kotlin/app/coilforphoniebox/ui/components/MiniPlayer.kt +++ b/app/src/main/kotlin/app/coilforphoniebox/ui/components/MiniPlayer.kt @@ -70,7 +70,7 @@ fun MiniPlayer( Spacer(Modifier.size(12.dp)) Column(Modifier.weight(1f)) { Text( - text = status.title ?: stringResource(R.string.player_no_title), + text = status.displayTitle ?: stringResource(R.string.player_no_title), style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/kotlin/app/coilforphoniebox/ui/player/PlayerScreen.kt b/app/src/main/kotlin/app/coilforphoniebox/ui/player/PlayerScreen.kt index daf52b6..6d07bae 100644 --- a/app/src/main/kotlin/app/coilforphoniebox/ui/player/PlayerScreen.kt +++ b/app/src/main/kotlin/app/coilforphoniebox/ui/player/PlayerScreen.kt @@ -303,7 +303,7 @@ private fun TitleBlock( Row(verticalAlignment = Alignment.Top) { Column(Modifier.weight(1f)) { Text( - text = state.status.title + text = state.status.displayTitle ?: if (state.status.hasContent) { stringResource(R.string.player_no_title) } else { diff --git a/app/src/main/kotlin/app/coilforphoniebox/ui/player/PlayerViewModel.kt b/app/src/main/kotlin/app/coilforphoniebox/ui/player/PlayerViewModel.kt index 670ca45..a8c1761 100644 --- a/app/src/main/kotlin/app/coilforphoniebox/ui/player/PlayerViewModel.kt +++ b/app/src/main/kotlin/app/coilforphoniebox/ui/player/PlayerViewModel.kt @@ -83,10 +83,11 @@ class PlayerViewModel @Inject constructor( /** What stand-in artwork is keyed on while this is playing — see [coverNameOf]. */ val coverName: String? get() = coverNameOf(status) - /** Track name as shown in the menu; the tags first, the file name as a fallback. */ - val trackLabel: String? get() = status.file?.let { file -> - status.title?.takeIf { it.isNotBlank() } ?: file.substringAfterLast('/') - } + /** + * Track name as shown in the menu — [PlayerStatus.displayTitle], but only for something + * that can actually be starred, which is a file and not a title on its own. + */ + val trackLabel: String? get() = if (status.file != null) status.displayTitle else null } private val messageChannel = MutableSharedFlow( diff --git a/core-domain/src/main/kotlin/app/coilforphoniebox/domain/model/PlayerStatus.kt b/core-domain/src/main/kotlin/app/coilforphoniebox/domain/model/PlayerStatus.kt index 8ec9c9e..da774d8 100644 --- a/core-domain/src/main/kotlin/app/coilforphoniebox/domain/model/PlayerStatus.kt +++ b/core-domain/src/main/kotlin/app/coilforphoniebox/domain/model/PlayerStatus.kt @@ -36,6 +36,19 @@ data class PlayerStatus( /** Folder the current song sits in, used to offer "save this as a favourite". */ val folder: String? get() = file?.substringBeforeLast('/', missingDelimiterValue = "")?.ifEmpty { null } + /** + * What to call the current track. Never blank while a file is loaded, which the media session + * depends on: media3 posts a notification with no content title for a null one, and the + * platform fills that hole with its own " is running". + * + * The tag first, then the file name — the same fallback `QueueParser` and + * [LibraryTrack.displayTitle] apply, and for the same reason: most of a Phoniebox library is + * untagged rips, so a missing title tag is the common case rather than the odd one. + */ + val displayTitle: String? + get() = title?.takeIf { it.isNotBlank() } + ?: file?.substringAfterLast('/')?.takeIf { it.isNotBlank() } + companion object { val Idle = PlayerStatus() } diff --git a/core-domain/src/test/kotlin/app/coilforphoniebox/domain/model/PlayerStatusTest.kt b/core-domain/src/test/kotlin/app/coilforphoniebox/domain/model/PlayerStatusTest.kt new file mode 100644 index 0000000..6a20ba9 --- /dev/null +++ b/core-domain/src/test/kotlin/app/coilforphoniebox/domain/model/PlayerStatusTest.kt @@ -0,0 +1,53 @@ +package app.coilforphoniebox.domain.model + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class PlayerStatusTest { + + @Test + fun `the title tag wins when the box sends one`() { + val status = PlayerStatus( + title = "Kapitel 3", + file = "Petronella/03 - Kapitel 3.mp3", + ) + assertEquals("Kapitel 3", status.displayTitle) + } + + /** + * The case that put "Coil is running" on the lock screen: most of a Phoniebox library is + * untagged rips, and a null title reaches media3 as a notification with no content title, + * which the platform fills in with its own text. + */ + @Test + fun `an untagged file falls back to its name`() { + val status = PlayerStatus(file = "Petronella/03 - Kapitel 3.mp3") + assertEquals("03 - Kapitel 3.mp3", status.displayTitle) + } + + /** A blank tag is the same as no tag; the parsers map both to null, but not every source does. */ + @Test + fun `a blank title falls back too`() { + val status = PlayerStatus(title = " ", file = "Petronella/03 - Kapitel 3.mp3") + assertEquals("03 - Kapitel 3.mp3", status.displayTitle) + } + + /** A file at the library root has no folder to strip off. */ + @Test + fun `a file with no folder is its own name`() { + assertEquals("kapitel.mp3", PlayerStatus(file = "kapitel.mp3").displayTitle) + } + + /** A stream has no file name, so there is nothing to fall back to but the tag. */ + @Test + fun `a trailing slash leaves nothing to fall back to`() { + assertNull(PlayerStatus(file = "http://stream.example/").displayTitle) + } + + /** An idle box names nothing, which is what every caller's own placeholder is for. */ + @Test + fun `an idle box has no title`() { + assertNull(PlayerStatus.Idle.displayTitle) + } +} diff --git a/fastlane/metadata/android/de-DE/changelogs/9.txt b/fastlane/metadata/android/de-DE/changelogs/9.txt new file mode 100644 index 0000000..6238820 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/9.txt @@ -0,0 +1 @@ +Ein Titel ohne Titel-Tag wird auf dem Sperrbildschirm und in der Benachrichtigung jetzt nach seiner Datei benannt. Dort stand vorher „Coil wird ausgeführt“, und das blieb stehen, während die Box durch das Album lief. Der Player und die Mini-Leiste benennen ihn genauso. diff --git a/fastlane/metadata/android/en-US/changelogs/9.txt b/fastlane/metadata/android/en-US/changelogs/9.txt new file mode 100644 index 0000000..23a54e1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/9.txt @@ -0,0 +1 @@ +A track with no title tag is now named by its file name on the lock screen and in the notification shade. Those controls used to read "Coil is running" instead, and kept reading it as the box moved through the album. The player screen and the mini player name such a track the same way. diff --git a/fastlane/metadata/android/es-ES/changelogs/9.txt b/fastlane/metadata/android/es-ES/changelogs/9.txt new file mode 100644 index 0000000..7ab2385 --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/9.txt @@ -0,0 +1 @@ +Una pista sin etiqueta de título ahora se nombra por su archivo en la pantalla de bloqueo y en las notificaciones. Antes, esos controles mostraban el nombre de la aplicación, y seguían así mientras la caja avanzaba por el álbum. La pantalla del reproductor y la minibarra la nombran igual. diff --git a/fastlane/metadata/android/fr-FR/changelogs/9.txt b/fastlane/metadata/android/fr-FR/changelogs/9.txt new file mode 100644 index 0000000..cf81ebb --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/9.txt @@ -0,0 +1 @@ +Une piste sans balise de titre est désormais nommée d'après son fichier sur l'écran de verrouillage et dans les notifications. Ces commandes affichaient le nom de l'application à la place, et le gardaient pendant que la boîte parcourait l'album. L'écran du lecteur et la mini-barre la nomment de même. diff --git a/fastlane/metadata/android/nl-NL/changelogs/9.txt b/fastlane/metadata/android/nl-NL/changelogs/9.txt new file mode 100644 index 0000000..190f684 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/9.txt @@ -0,0 +1 @@ +Een nummer zonder titeltag krijgt op het vergrendelscherm en in de meldingen nu de naam van zijn bestand. Daar stond eerder de naam van de app, en die bleef staan terwijl de box door het album liep. Het spelerscherm en de mini-balk noemen het net zo. diff --git a/feature-media/src/main/kotlin/app/coilforphoniebox/media/PhonieboxPlayer.kt b/feature-media/src/main/kotlin/app/coilforphoniebox/media/PhonieboxPlayer.kt index 494250b..e0e6d01 100644 --- a/feature-media/src/main/kotlin/app/coilforphoniebox/media/PhonieboxPlayer.kt +++ b/feature-media/src/main/kotlin/app/coilforphoniebox/media/PhonieboxPlayer.kt @@ -343,7 +343,10 @@ class PhonieboxPlayer( ?: C.TIME_UNSET val metadata = MediaMetadata.Builder() - .setTitle(status.title) + // Not `status.title`: the box leaves the tag out for an untagged rip, and a null title + // is not an empty line on the lock screen — media3 posts a notification with no + // content title and the platform substitutes " is running" for it. + .setTitle(status.displayTitle) .setArtist(status.artist) .setAlbumTitle(status.album) // With more than one box, the lock screen has to make clear which device is