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