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 fe736f42..597946d6 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 @@ -166,12 +166,19 @@ class DatabaseRepo( // entries) instead of admitting every alias (DESTINY etc.). return setOf(all.minOrNull() ?: all.first()) } - // A failed FM/Linear page fetch yields an empty list here; keep the - // previous lists so a single network hiccup cannot wipe the filters. + // A failed FM/Linear page fetch yields an empty list here; do NOT + // keep the stale list verbatim (it may predate the whitelist and + // still contain ISS module aliases like DESTINY). Instead re-run + // the multi-match disambiguation on the previous catnums via their + // local names, so the whitelist / smallest-catnum rules clean it. + fun cleanStaleList(previous: Set): Set = + previous.mapNotNull { catnum -> + nameToCatnum.entries.firstOrNull { it.value == catnum }?.key + }.flatMap { resolvePerName(it) }.toSet() val fmCatnums = fmNames.flatMap { resolvePerName(it) }.toSet() - .ifEmpty { settingsRepo.getAmSatFmCatnums() } + .ifEmpty { cleanStaleList(settingsRepo.getAmSatFmCatnums()) } val linearCatnums = linearNames.flatMap { resolvePerName(it) }.toSet() - .ifEmpty { settingsRepo.getAmSatLinearCatnums() } + .ifEmpty { cleanStaleList(settingsRepo.getAmSatLinearCatnums()) } 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/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt index f343d03f..7fc8f005 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SelectionRepo.kt @@ -25,6 +25,7 @@ import com.rtbishop.look4sat.core.domain.source.Sources import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -45,17 +46,21 @@ class SelectionRepo( // The HashSet gives O(1) catnum lookups instead of O(n) with a List. // The three virtual types ("AMSAT Live FM", "AMSAT Live Linear", "Live SSTV") // are resolved from live transponder data instead of SharedPreferences. - private val itemsWithTypes = currentTypes.flatMapLatest { types: List -> - val catnumSet: Set? = if (types.isEmpty()) { - null // null = no filtering - } else { - val ids = resolveTypeIds(types) - if (ids.isEmpty()) emptySet() else ids.toHashSet() + // The AMSAT list version counter re-triggers resolution when a background + // data sync rewrites the FM/Linear lists, so the UI reflects the new list + // without a restart or type toggle. + private val itemsWithTypes = combine(currentTypes, settingsRepo.amSatListsVersion) { types, _ -> types } + .flatMapLatest { types: List -> + val catnumSet: Set? = if (types.isEmpty()) { + null // null = no filtering + } else { + val ids = resolveTypeIds(types) + if (ids.isEmpty()) emptySet() else ids.toHashSet() + } + currentItems.map { items -> + if (catnumSet == null) items else items.filter { it.catnum in catnumSet } + } } - currentItems.map { items -> - if (catnumSet == null) items else items.filter { it.catnum in catnumSet } - } - } private val itemsWithQuery = currentQuery.flatMapLatest { query -> itemsWithTypes.map { items -> filterByQuery(items, query) } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt index bac25c18..f4cafde3 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt @@ -749,6 +749,9 @@ class SettingsRepo( //endregion //region # AMSAT live-transponder lists + private val _amSatListsVersion = MutableStateFlow(0) + override val amSatListsVersion: StateFlow = _amSatListsVersion + override fun getAmSatFmCatnums(): Set { val raw = preferences.getString(keyAmSatFmCatnums, null) ?: return emptySet() return raw.split(separatorComma).mapNotNull { it.toIntOrNull() }.toSet() @@ -764,6 +767,7 @@ class SettingsRepo( putString(keyAmSatFmCatnums, fmCatnums.sorted().joinToString(separatorComma)) putString(keyAmSatLinearCatnums, linearCatnums.sorted().joinToString(separatorComma)) } + _amSatListsVersion.update { it + 1 } } override fun getAmSatActiveCatnums(): Set { 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 dd2dc318..38475503 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 @@ -195,24 +195,30 @@ class DatabaseRepoTest { } @Test - fun `amsat fm list keeps previous list when fm page fetch fails`() = runTest(dispatcher) { + fun `amsat fm list cleans stale ISS module aliases when fm page fetch fails`() = runTest(dispatcher) { val satnogsUrl = Sources.satelliteDataUrls.getValue("SatNOGS") val localSource = FakeLocalSource() val remoteSource = FakeRemoteSource().apply { - networkStreams[satnogsUrl] = { jamxTleStream() } - // FM page NOT registered -> 404 -> empty parse -> previous list retained. + networkStreams[satnogsUrl] = { issModulesCsvStream() } + // FM page NOT registered -> 404 -> empty parse. } val settingsRepo = FakeSettingsRepo( dataSources = DataSourcesSettings( satelliteUrls = listOf(satnogsUrl), transceiversUrls = emptyList() ) - ).apply { amSatFm = setOf(25544, 7530) } // list from an earlier good sync + ).apply { + // Pre-whitelist era stale list: ALL five ISS module entries. + amSatFm = setOf(25544, 25575, 26400, 26700, 49044) + } val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo) repository.updateFromRemote() - assertEquals(setOf(25544, 7530), settingsRepo.amSatFm) + // The stale list is not kept verbatim: with the FM page down, the + // previous catnums are re-disambiguated via local names, collapsing + // the five ISS modules to the primary ZARYA (25544). + assertEquals(setOf(25544), settingsRepo.amSatFm) } private fun issModulesCsvStream(): InputStream = """ @@ -365,6 +371,7 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou var amSatFm: Set = emptySet() var amSatLinear: Set = emptySet() var amSatActive: Set = emptySet() + override val amSatListsVersion: StateFlow = MutableStateFlow(0) override fun getAmSatFmCatnums(): Set = amSatFm override fun getAmSatLinearCatnums(): Set = amSatLinear diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt index 01b4d1a9..6cead0d9 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt @@ -217,6 +217,29 @@ class SelectionRepoSearchTest { assertTrue("All" in types) } + @Test + fun `amsat list version bump re-resolves FM filter after data sync`() = runTest { + // ISS module aliases: dirty pre-sync FM list contains both ZARYA and DESTINY. + val items = listOf( + SatItem(25544, "ISS (ZARYA)"), + SatItem(26700, "ISS (DESTINY)") + ) + val fake = FakeSettingsRepoForSearch(amSatFm = setOf(25544, 26700)) + val repo = SelectionRepo( + dispatcher = Dispatchers.Unconfined, + localSource = FakeLocalSourceForSearch(items), + settingsRepo = fake + ) + repo.setTypes(listOf("AMSAT Live FM")) + assertEquals(setOf(25544, 26700), repo.getEntriesFlow().first().map { it.catnum }.toSet()) + + // Simulate a background data sync: lists rewritten + version bumped. + fake.setAmSatCatnums(fmCatnums = setOf(25544), linearCatnums = emptySet()) + + // The FM filter must reflect the new list WITHOUT a type toggle or restart. + assertEquals(listOf(25544), repo.getEntriesFlow().first().map { it.catnum }) + } + private fun createRepo( items: List, amSatFm: Set = emptySet(), @@ -252,8 +275,8 @@ private class FakeLocalSourceForSearch( } private class FakeSettingsRepoForSearch( - private val amSatFm: Set = emptySet(), - private val amSatLinear: Set = emptySet(), + var amSatFm: Set = emptySet(), + var amSatLinear: Set = emptySet(), private var amSatActive: Set = emptySet() ) : ISettingsRepo { override val appVersionName: String = "test" @@ -299,7 +322,12 @@ private class FakeSettingsRepoForSearch( override fun updateDataSourcesStatus(status: Map) = Unit override fun getAmSatFmCatnums(): Set = amSatFm override fun getAmSatLinearCatnums(): Set = amSatLinear - override fun setAmSatCatnums(fmCatnums: Set, linearCatnums: Set) = Unit + override val amSatListsVersion: StateFlow = MutableStateFlow(0) + override fun setAmSatCatnums(fmCatnums: Set, linearCatnums: Set) { + amSatFm = fmCatnums + amSatLinear = linearCatnums + (amSatListsVersion as MutableStateFlow).value++ + } override fun getAmSatActiveCatnums(): Set = amSatActive override fun setAmSatActiveCatnums(catnums: Set) { amSatActive = catnums } override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt index 0e2f726e..56929dff 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt @@ -89,6 +89,11 @@ interface ISettingsRepo { * active-transponder table (whitelist for the virtual type filters). */ fun getAmSatActiveCatnums(): Set fun setAmSatActiveCatnums(catnums: Set) + /** Monotonic version counter, incremented every time the AMSAT live + * FM/Linear lists are rewritten. Consumers that resolve the virtual + * types (SelectionRepo) should re-resolve on change so a background + * data sync is reflected in the UI without a restart. */ + val amSatListsVersion: StateFlow //endregion //region # Radio control settings diff --git a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/repository/LoTWGridSyncTest.kt b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/repository/LoTWGridSyncTest.kt index b1e95490..4299f0ce 100644 --- a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/repository/LoTWGridSyncTest.kt +++ b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/repository/LoTWGridSyncTest.kt @@ -285,6 +285,7 @@ class LoTWGridSyncTest { override fun updateDataSourcesStatus(status: Map) = TODO() override fun getAmSatFmCatnums(): Set = emptySet() override fun getAmSatLinearCatnums(): Set = emptySet() + override val amSatListsVersion: StateFlow = MutableStateFlow(0) override fun setAmSatCatnums(fmCatnums: Set, linearCatnums: Set) = Unit override fun getAmSatActiveCatnums(): Set = emptySet() override fun setAmSatActiveCatnums(catnums: Set) = Unit diff --git a/feature/mutual/src/test/java/com/rtbishop/look4sat/feature/mutual/Fakes.kt b/feature/mutual/src/test/java/com/rtbishop/look4sat/feature/mutual/Fakes.kt index 84ffc185..514dbf33 100644 --- a/feature/mutual/src/test/java/com/rtbishop/look4sat/feature/mutual/Fakes.kt +++ b/feature/mutual/src/test/java/com/rtbishop/look4sat/feature/mutual/Fakes.kt @@ -112,6 +112,7 @@ class FakeSettingsRepo( override fun getAmSatFmCatnums(): Set = amSatFm override fun getAmSatLinearCatnums(): Set = amSatLinear + override val amSatListsVersion: StateFlow = MutableStateFlow(0) override fun setAmSatCatnums(fmCatnums: Set, linearCatnums: Set) = TODO() override fun getAmSatActiveCatnums(): Set = emptySet() override fun setAmSatActiveCatnums(catnums: Set) = TODO()