From 865fa9c6f20495798041d8cee2fbe458a6230a8b Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Thu, 24 Sep 2026 11:37:50 +0800 Subject: [PATCH] fix: AMSAT live page timeouts wiped FM/Linear lists; add fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of the recurring empty AMSAT Live filters: OkHttp defaults to a 10s read timeout and amsat.org's live pages take 7-10s+ from many networks — every sync timed out, fmNames/linearNames stayed empty, and (with no previous list, e.g. right after clearing data) the filters were written as EMPTY. The stale pre-whitelist FM list survived only because older syncs had occasionally succeeded and the failure path preserved it. Fix: 1. OkHttp timeouts raised (connect 20s / read 30s / write 30s / call 60s) in MainContainer.provideRemoteSource so slow AMSAT pages actually load. 2. updateAmSatLiveLists gains a last-resort fallback: when the AMSAT page is down AND there is no previous list to clean, derive FM/Linear from the local transceivers (FM / SSB,CW,USB,LSB modes) intersected with the amateur whitelist, so a network timeout never wipes the filters. ISS module aliases stay excluded by the whitelist intersection. Regression test: page fails + empty previous list -> fallback yields {25544} (26700 excluded by whitelist). --- .../core/data/injection/MainContainer.kt | 12 ++++++- .../core/data/repository/DatabaseRepo.kt | 10 ++++++ .../core/data/repository/DatabaseRepoTest.kt | 34 +++++++++++++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt index 03114576..1a9454e9 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/injection/MainContainer.kt @@ -74,6 +74,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import okhttp3.OkHttpClient +import java.util.concurrent.TimeUnit class MainContainer(private val context: Context) : IMainContainer { @@ -177,7 +178,16 @@ class MainContainer(private val context: Context) : IMainContainer { } private fun provideRemoteSource(): IRemoteSource { - return RemoteSource(Dispatchers.IO, context.contentResolver, OkHttpClient.Builder().build()) + // amsat.org live pages are slow (7-10s+ from some networks); the + // OkHttp defaults (10s read timeout) made every AMSAT Live update + // time out, silently keeping stale FM/Linear lists forever. + val client = OkHttpClient.Builder() + .connectTimeout(20, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .callTimeout(60, TimeUnit.SECONDS) + .build() + return RemoteSource(Dispatchers.IO, context.contentResolver, client) } private fun provideSatelliteRepo(): ISatelliteRepo { diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt index 4d59aef9..35f6092c 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepo.kt @@ -181,10 +181,20 @@ class DatabaseRepo( previous.mapNotNull { catnum -> nameToCatnum.entries.firstOrNull { it.value == catnum }?.key }.flatMap { resolvePerName(it) }.toSet() + // Last-resort fallback when the AMSAT page is down AND there is no + // previous list to clean (e.g. right after clearing data): derive + // the FM/Linear sets from the local transceivers intersected with + // the amateur whitelist, so a network timeout never wipes the + // filters to empty. + val fmFallback = localSource.getIdsWithModes(listOf("FM")).toSet().intersect(activeCatnums) + val linearFallback = localSource + .getIdsWithModes(listOf("SSB", "CW", "USB", "LSB")).toSet().intersect(activeCatnums) val fmCatnums = fmNames.flatMap { resolvePerName(it) }.toSet() .ifEmpty { cleanStaleList(settingsRepo.getAmSatFmCatnums()) } + .ifEmpty { fmFallback } val linearCatnums = linearNames.flatMap { resolvePerName(it) }.toSet() .ifEmpty { cleanStaleList(settingsRepo.getAmSatLinearCatnums()) } + .ifEmpty { linearFallback } settingsRepo.setAmSatCatnums(fmCatnums, linearCatnums) println("AMSAT live lists updated: FM=${fmCatnums.size}, Linear=${linearCatnums.size}, Active=${activeCatnums.size}") }.onFailure { diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt index d950a44b..e37e8269 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt @@ -270,6 +270,35 @@ class DatabaseRepoTest { assertEquals(setOf(25544), settingsRepo.amSatFm) } + @Test + fun `amsat fm list falls back to local FM transceivers when page fails and no previous list`() = + runTest(dispatcher) { + // After clearing data, the previous FM list is empty AND the AMSAT + // page is down (timeout) -> the filter must fall back to local FM + // transceivers intersected with the whitelist, never stay empty. + val satnogsUrl = Sources.satelliteDataUrls.getValue("SatNOGS") + val localSource = FakeLocalSource().apply { + idsWithModes = listOf(25544, 26700, 39444) // local FM-transceiver sats + } + val remoteSource = FakeRemoteSource().apply { + networkStreams[satnogsUrl] = { issModulesCsvStream() } + // FM/Linear pages NOT registered -> 404 -> fetch fails. + } + val settingsRepo = FakeSettingsRepo( + dataSources = DataSourcesSettings( + satelliteUrls = listOf(satnogsUrl), + transceiversUrls = emptyList() + ) + ).apply { amSatActive = setOf(25544) } // whitelist has only ZARYA + val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo) + + repository.updateFromRemote() + + // Fallback = local FM ids ∩ whitelist = {25544}; ISS module aliases + // (26700) are excluded by the whitelist. + assertEquals(setOf(25544), settingsRepo.amSatFm) + } + private fun amsatFmPageWithUnknownStream(): InputStream = """ @@ -320,6 +349,7 @@ private class FakeRemoteSource : IRemoteSource { private class FakeLocalSource : ILocalSource { val insertedEntries = mutableListOf() private val insertedRadios = mutableListOf() + var idsWithModes: List = emptyList() override suspend fun getEntriesTotal(): Int = insertedEntries.size @@ -336,8 +366,8 @@ private class FakeLocalSource : ILocalSource { insertedEntries.clear() } - override suspend fun getIdsWithModes(modes: List): List = emptyList() - override suspend fun getIdsWithModesAndUplink(modes: List): List = emptyList() + override suspend fun getIdsWithModes(modes: List): List = idsWithModes + override suspend fun getIdsWithModesAndUplink(modes: List): List = idsWithModes override suspend fun getIdsWithModesAndAmateur(modes: List): List = emptyList() override suspend fun getRadiosTotal(): Int = insertedRadios.size
SatelliteUplinkDownlinkComment