address review: localize connection errors, tighten i18n guard, drop dead resources

- AddSourceViewModel resolves SubsonicException.uiMessage too, so malformed/
  HTTP/truncated responses show localized text instead of the raw parser
  message; add a regression test.
- StringResourcesTest now requires an 'other' quantity in every plural and
  asserts exact placeholder (index and conversion) equality, both of which
  could previously pass while crashing getQuantityString at runtime.
- Map the pre-enum song_downloads.error sentences back to their DownloadError
  so existing failed rows keep their specific message.
- Localize the media-session star/unstar button label.
- Remove 12 unreferenced resources (queue name, filter year, sync_failed, and
  the unused codec_* strings; codec labels stay in AudioEncoding).
- Make buildTree's fallback arguments required; only tests used the defaults.
This commit is contained in:
austinried
2026-10-04 00:30:07 +00:00
parent 3162ac07d5
commit 699a6e86f5
33 changed files with 59 additions and 55 deletions
@@ -130,5 +130,25 @@ class DownloadErrorConverter {
fun fromDownloadError(error: DownloadError): String = error.name
@ColumnTypeConverter
fun toDownloadError(value: String): DownloadError = DownloadError.entries.firstOrNull { it.name == value } ?: DownloadError.Failed
fun toDownloadError(value: String): DownloadError =
DownloadError.entries.firstOrNull { it.name == value }
?: LEGACY_DOWNLOAD_ERRORS[value]
?: DownloadError.Failed
}
private val LEGACY_DOWNLOAD_ERRORS =
mapOf(
"Not enough space to download" to DownloadError.NoSpace,
"Storage is unavailable" to DownloadError.StorageUnavailable,
"Could not write the download" to DownloadError.WriteFailed,
"The server refused to send this track" to DownloadError.ServerRefused,
"The server redirected too many times" to DownloadError.TooManyRedirects,
"The download could not be resumed" to DownloadError.ResumeFailed,
"Download failed" to DownloadError.Failed,
"The downloaded file is missing" to DownloadError.FileMissing,
"The server sent an error instead of the track" to DownloadError.ServerError,
"Can't download: the server address is unavailable" to DownloadError.NoAddress,
"Nothing left to download from this list" to DownloadError.NothingLeft,
"The download stopped unexpectedly" to DownloadError.Stopped,
"Can't download from a source that isn't active" to DownloadError.InactiveSource,
)
@@ -2,7 +2,6 @@ package com.subtracks.data.sync
import android.util.Log
import com.subtracks.R
import com.subtracks.UiException
import com.subtracks.UiMessage
import com.subtracks.data.db.SubtracksDatabase
import com.subtracks.data.repo.QueueRepository
@@ -73,10 +72,6 @@ class SyncManager(
UiMessage(R.string.error_no_server)
}
is UiException -> {
failure.uiMessage
}
else -> {
(failure as? SubsonicException)?.uiMessage
?: UiMessage(R.string.sync_failed_detail, listOf(failure.message ?: ""))
@@ -28,6 +28,7 @@ import coil3.SingletonImageLoader
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.subtracks.MainActivity
import com.subtracks.R
import com.subtracks.data.repo.LibraryRepository
import com.subtracks.data.repo.SourceRepository
import com.subtracks.data.source.StarType
@@ -176,7 +177,7 @@ class PlaybackService : MediaSessionService() {
CommandButton
.Builder(if (isStarred) CommandButton.ICON_STAR_FILLED else CommandButton.ICON_STAR_UNFILLED)
.setSessionCommand(STAR_COMMAND)
.setDisplayName(if (isStarred) "Unstar" else "Star")
.setDisplayName(getString(if (isStarred) R.string.actions_unstar else R.string.actions_star))
.build()
private fun streamingClient(): OkHttpClient =
@@ -93,8 +93,8 @@ class DownloadsViewModel(
internal fun buildTree(
songs: List<DownloadedSong>,
unknownArtist: String = "Unknown artist",
unknownAlbum: String = "Unknown album",
unknownArtist: String,
unknownAlbum: String,
): DownloadTree =
DownloadTree(
artists =
@@ -7,6 +7,7 @@ import com.subtracks.R
import com.subtracks.UiException
import com.subtracks.data.repo.DownloadRepository
import com.subtracks.data.repo.SourceRepository
import com.subtracks.data.source.subsonic.SubsonicException
import com.subtracks.data.sync.SyncManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -101,8 +102,8 @@ class AddSourceViewModel(
}
},
{ error ->
(error as? UiException)?.uiMessage?.resolve(resources)
?: resources.getString(R.string.connection_failed, error.message)
val ui = (error as? UiException)?.uiMessage ?: (error as? SubsonicException)?.uiMessage
ui?.resolve(resources) ?: resources.getString(R.string.connection_failed, error.message)
},
),
isError = result.isFailure,
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Artista</string>
<string name="resources_filter_genre">Gênero</string>
<string name="resources_filter_starred">Estrelado por</string>
<string name="resources_filter_year">Ano</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d song</item>
<item quantity="other">%1$d songs</item>
@@ -16,7 +16,6 @@
<string name="resources_filter_artist">歌手</string>
<string name="resources_filter_genre">根据类型</string>
<string name="resources_filter_starred">已收藏</string>
<string name="resources_filter_year">年份</string>
<plurals name="resources_song_count">
<item quantity="other">%1$d 首歌曲</item>
</plurals>
@@ -16,7 +16,6 @@
<string name="resources_filter_artist">歌手</string>
<string name="resources_filter_genre">根據類型</string>
<string name="resources_filter_starred">已收藏</string>
<string name="resources_filter_year">年份</string>
<plurals name="resources_song_count">
<item quantity="other">%1$d 首歌曲</item>
</plurals>
-1
View File
@@ -13,7 +13,6 @@
<string name="resources_filter_artist">Umělec</string>
<string name="resources_filter_genre">Podle žánru</string>
<string name="resources_filter_starred">Ohodnocené</string>
<string name="resources_filter_year">Rok</string>
<string name="resources_sort_by_artist">Umělce</string>
<string name="resources_sort_by_frequently_played">Často přehrávané</string>
<string name="resources_sort_by_name">Názvu</string>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Künstler</string>
<string name="resources_filter_genre">Nach Genre</string>
<string name="resources_filter_starred">Favoriten</string>
<string name="resources_filter_year">Jahr</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d Song</item>
<item quantity="other">%1$d Songs</item>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Artista</string>
<string name="resources_filter_genre">Género</string>
<string name="resources_filter_starred">Estrellas</string>
<string name="resources_filter_year">Año</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d Canción</item>
<item quantity="other">%1$d Canciones</item>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Musikaria</string>
<string name="resources_filter_genre">Generoa</string>
<string name="resources_filter_starred">Gogokoak</string>
<string name="resources_filter_year">Urtea</string>
<plurals name="resources_song_count">
<item quantity="one">Kanta bat</item>
<item quantity="other">%1$d kanta</item>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Artiste</string>
<string name="resources_filter_genre">Par Genre</string>
<string name="resources_filter_starred">Favoris</string>
<string name="resources_filter_year">Année</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d chanson</item>
<item quantity="other">%1$d chansons</item>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Artista</string>
<string name="resources_filter_genre">Por xénero</string>
<string name="resources_filter_starred">Favoritas</string>
<string name="resources_filter_year">Ano</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d canción</item>
<item quantity="other">%1$d cancións</item>
-1
View File
@@ -16,7 +16,6 @@
<string name="resources_filter_artist">Előadó</string>
<string name="resources_filter_genre">Stílus</string>
<string name="resources_filter_starred">Kedvencek között</string>
<string name="resources_filter_year">Év</string>
<string name="resources_sort_by_artist">Előadó</string>
<string name="resources_sort_by_frequently_played">Gyakran játszott</string>
<string name="resources_sort_by_name">Név</string>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Artista</string>
<string name="resources_filter_genre">Per genere</string>
<string name="resources_filter_starred">Preferiti</string>
<string name="resources_filter_year">Anno</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d brano</item>
<item quantity="other">%1$d brani</item>
-1
View File
@@ -8,7 +8,6 @@
<string name="navigation_tabs_settings">設定</string>
<string name="resources_filter_album">アルブム</string>
<string name="resources_filter_starred">星付きアルバム</string>
<string name="resources_filter_year">年</string>
<string name="resources_sort_by_frequently_played">よく聴くアルバム</string>
<string name="resources_sort_by_recently_played">最近再生した</string>
<string name="search_input_placeholder">検索</string>
-1
View File
@@ -16,7 +16,6 @@
<string name="resources_filter_artist">아티스트</string>
<string name="resources_filter_genre">장르</string>
<string name="resources_filter_starred">평가됨</string>
<string name="resources_filter_year">년</string>
<plurals name="resources_song_count">
<item quantity="other">%1$d 노래</item>
</plurals>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Artist</string>
<string name="resources_filter_genre">Etter sjanger</string>
<string name="resources_filter_starred">Stjernemerket</string>
<string name="resources_filter_year">År</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d sang</item>
<item quantity="other">%1$d sanger</item>
-1
View File
@@ -13,7 +13,6 @@
<string name="resources_filter_artist">Artysta</string>
<string name="resources_filter_genre">Wg gatunku</string>
<string name="resources_filter_starred">Ulubione</string>
<string name="resources_filter_year">Rok</string>
<string name="resources_sort_by_artist">Wg wykonawcy</string>
<string name="resources_sort_by_frequently_played">Często odtwarzane</string>
<string name="resources_sort_by_name">Wg nazwy</string>
-1
View File
@@ -13,7 +13,6 @@
<string name="resources_filter_artist">Artista</string>
<string name="resources_filter_genre">Por Género</string>
<string name="resources_filter_starred">Favoritos</string>
<string name="resources_filter_year">Ano</string>
<string name="resources_sort_by_artist">Por Artista</string>
<string name="resources_sort_by_frequently_played">Mais Tocado</string>
<string name="resources_sort_by_name">Por Nome</string>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">По исполнителю</string>
<string name="resources_filter_genre">По жанру</string>
<string name="resources_filter_starred">Избранные</string>
<string name="resources_filter_year">По годам</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d трек</item>
<item quantity="few">%1$d треков</item>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Artist</string>
<string name="resources_filter_genre">Genre</string>
<string name="resources_filter_starred">Markerad</string>
<string name="resources_filter_year">År</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d låt</item>
<item quantity="other">%1$d låtar</item>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">கலைஞர்</string>
<string name="resources_filter_genre">வகை</string>
<string name="resources_filter_starred">நடித்தார்</string>
<string name="resources_filter_year">ஆண்டு</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d பாடல்</item>
<item quantity="other">%1$d பாடல்கள்</item>
-1
View File
@@ -16,7 +16,6 @@
<string name="resources_filter_artist">ศิลปิน</string>
<string name="resources_filter_genre">ประเภท</string>
<string name="resources_filter_starred">ให้ดาว</string>
<string name="resources_filter_year">ปี</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d เพลง</item>
<item quantity="other">%1$d เพลง</item>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Sanatçı</string>
<string name="resources_filter_genre">Türe göre</string>
<string name="resources_filter_starred">Yıldızlı</string>
<string name="resources_filter_year">Yıl</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d şarkı</item>
<item quantity="other">%1$d şarkı</item>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Виконавець</string>
<string name="resources_filter_genre">Жанр</string>
<string name="resources_filter_starred">Улюблене</string>
<string name="resources_filter_year">Рік</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d пісня</item>
<item quantity="other">%1$d пісні</item>
-1
View File
@@ -17,7 +17,6 @@
<string name="resources_filter_artist">Theo nghệ sĩ</string>
<string name="resources_filter_genre">Theo thể loại</string>
<string name="resources_filter_starred">Theo yêu thích</string>
<string name="resources_filter_year">Theo năm</string>
<plurals name="resources_song_count">
<item quantity="one">%1$d bài hát</item>
<item quantity="other">%1$d bài hát</item>
-12
View File
@@ -27,12 +27,10 @@
<string name="resources_album_name">Albums</string>
<string name="resources_artist_name">Artists</string>
<string name="resources_playlist_name">Playlists</string>
<string name="resources_queue_name">Queue</string>
<string name="resources_filter_album">Album</string>
<string name="resources_filter_artist">Artist</string>
<string name="resources_filter_genre">Genre</string>
<string name="resources_filter_starred">Starred</string>
<string name="resources_filter_year">Year</string>
<string name="resources_song_list_delete_all_content">%1$s\n\nDeleting will free %2$s</string>
<string name="resources_song_list_delete_all_title">Delete downloads</string>
<string name="resources_sort_by_added">Added</string>
@@ -222,7 +220,6 @@
<string name="star_update_failed">Could not update star</string>
<string name="sync_offline">Offline mode is on</string>
<string name="error_no_server">No server configured</string>
<string name="sync_failed">Sync failed</string>
<string name="sync_failed_detail">Sync failed: %1$s</string>
<string name="error_download_no_space">Not enough space to download</string>
@@ -286,13 +283,4 @@
<item quantity="other">%1$d plays</item>
</plurals>
<string name="codec_mp3" translatable="false">MP3</string>
<string name="codec_aac" translatable="false">AAC</string>
<string name="codec_alac" translatable="false">ALAC</string>
<string name="codec_flac" translatable="false">FLAC</string>
<string name="codec_ogg" translatable="false">Ogg</string>
<string name="codec_vorbis" translatable="false">Vorbis</string>
<string name="codec_opus" translatable="false">Opus</string>
<string name="codec_webm" translatable="false">WebM</string>
<string name="codec_wav" translatable="false">WAV</string>
</resources>
@@ -24,6 +24,12 @@ class StringResourcesTest {
val default = parse(defaultFile)
assertTrue("default strings.xml should not be empty", default.isNotEmpty())
default.forEach { (name, resource) ->
if (resource.kind == "plurals") {
assertTrue("$name in values/strings.xml is missing the required 'other' quantity", "other" in resource.quantities)
}
}
localeFiles().forEach { file ->
val locale = parse(file)
locale.forEach { (name, resource) ->
@@ -35,12 +41,12 @@ class StringResourcesTest {
"$name has an invalid quantity in ${file.parentFile.name}",
resource.quantities.all { it in validQuantities },
)
assertTrue("$name in ${file.parentFile.name} is missing the required 'other' quantity", "other" in resource.quantities)
}
val baseArgs = placeholderIndices(base.text)
val localeArgs = placeholderIndices(resource.text)
assertTrue(
"$name in ${file.parentFile.name} uses placeholders $localeArgs not present in the default $baseArgs",
baseArgs.containsAll(localeArgs),
assertEquals(
"$name placeholders differ in ${file.parentFile.name}",
placeholders(base.text),
placeholders(resource.text),
)
}
}
@@ -84,6 +90,6 @@ class StringResourcesTest {
return (0 until items.length).joinToString(" ") { items.item(it).textContent }
}
private fun placeholderIndices(text: String): Set<Int> =
Regex("""%(\d+)\$""").findAll(text).mapTo(mutableSetOf()) { it.groupValues[1].toInt() }
private fun placeholders(text: String): Map<Int, Char> =
Regex("""%(\d+)\$([a-zA-Z])""").findAll(text).associate { it.groupValues[1].toInt() to it.groupValues[2][0] }
}
@@ -17,6 +17,8 @@ class DownloadTreeTest {
song("s3", "Three", "ar1", "Artist", "al2", "Second", size = 5),
song("s4", "Four", "ar2", "Other", "al3", "Third", size = 1),
),
"Unknown artist",
"Unknown album",
)
assertEquals(listOf("Artist", "Other"), tree.artists.map { it.name })
@@ -37,7 +39,7 @@ class DownloadTreeTest {
@Test
fun aSongWithNoArtistOrAlbumIsStillShown() {
val tree = buildTree(listOf(song("s1", "One", null, null, null, null, size = 3)))
val tree = buildTree(listOf(song("s1", "One", null, null, null, null, size = 3)), "Unknown artist", "Unknown album")
val artist = tree.artists.single()
assertEquals("Unknown artist", artist.name)
@@ -104,6 +104,20 @@ class AddSourceViewModelTest {
assertEquals("Failed: Wrong username", state.message)
}
@Test
fun aMalformedResponseIsReportedWithTheLocalizedMessage() {
val viewModel = viewModel()
viewModel.setAddress(server.url("/").toString())
viewModel.setUsername("u")
viewModel.setPassword("p")
server.enqueue(MockResponse().setBody("not an xml document"))
viewModel.testConnection()
await { !viewModel.state.value.busy && viewModel.state.value.isError }
assertEquals("Malformed response from the server", viewModel.state.value.message)
}
@Test
fun aSuccessfulPingClearsAnEarlierError() {
val viewModel = viewModel()