Compare commits

...
Author SHA1 Message Date
atsunatsu e1daf70f11 build: bump version to 4.4.7-ba7opf.15.6 (503) 2026-09-24 10:48:46 +08:00
atsunatsu ca9f93c1ed fix: persist All source type ids so the All filter shows satellites
Root cause of "All filter is empty": 'All' is a real TLE source type
(CelesTrak active group) in satelliteDataUrls, but setSatelliteTypeIds
had an upstream-legacy 'if (type == "All") return' that silently dropped
its ids on every sync. Selecting 'All' in the type dialog then resolved
to an empty catnum set -> empty list, while entries stayed populated
(which is why satellite counts looked normal).

Also fix getTypesList: removeAt(0) removed the alphabetically-first real
source ('ARISS') instead of the intended placeholder; now excludes only
'Other' (empty URL), so all real source types incl. 'All'/'ARISS' are
selectable.

Tests: remote update persists All ids; All type resolves to persisted
CelesTrak active ids; getTypesList excludes only Other.
2026-09-24 10:48:22 +08:00
atsunatsu 42bd55e180 build: bump version to 4.4.7-ba7opf.15.5 (502) 2026-09-24 09:40:56 +08:00
atsunatsu 7401e1451b fix: re-resolve AMSAT virtual types after sync; clean stale FM lists
Two independent defects kept ISS (DESTINY) visible in the satellite-page
AMSAT Live FM filter even after installing the fix and syncing data:

1. UI-layer staleness (SelectionRepo): resolveTypeIds ran only when the
   filter type changed and snapshotted the catnum set. A background data
   sync rewriting the FM/Linear lists never re-triggered resolution, so
   the UI kept filtering with the pre-sync (dirty) set until restart or a
   type toggle. ISettingsRepo now exposes a monotonic amSatListsVersion
   StateFlow, bumped by SettingsRepo.setAmSatCatnums; SelectionRepo
   combines it with currentTypes so every rewrite re-resolves instantly.

2. Data-layer staleness (DatabaseRepo): when the AMSAT FM page fetch
   failed, the previous list was kept verbatim — including pre-whitelist
   era lists containing all five ISS module entries. The stale list is now
   re-disambiguated: previous catnums are looked up by local name and run
   through the same whitelist / smallest-catnum collapse, so ISS modules
   always collapse to the primary ZARYA (25544).
2026-09-24 09:40:42 +08:00
atsunatsu fa51fb2b44 build: bump version to 4.4.7-ba7opf.15.4 (501) 2026-09-24 09:17:55 +08:00
atsunatsu 75a251eb98 fix: keep last good AMSAT live lists on fetch failures; whitelist mirror
- Failed whitelist fetch (raw.githubusercontent.com unreachable on some
  networks) no longer wipes the persisted whitelist or admits every ISS
  module alias: fall back to the persisted whitelist, and only persist
  when a stream was actually fetched.
- Multi-match collapse without any whitelist now keeps the primary entry
  (smallest catnum, ISS ZARYA=25544) instead of every alias (DESTINY etc.).
- Failed FM/Linear page fetch keeps the previous lists instead of
  clearing the filters.
- Amateur whitelist gets a jsDelivr CDN mirror as fallback source.
2026-09-24 09:17:17 +08:00
10 changed files with 294 additions and 33 deletions

No files matched your search

@@ -23,6 +23,7 @@ import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.core.domain.source.ILocalSource
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
import com.rtbishop.look4sat.core.domain.source.NetworkResult
import com.rtbishop.look4sat.core.domain.source.Sources
import com.rtbishop.look4sat.core.domain.utility.DataParser
import kotlinx.coroutines.CoroutineDispatcher
@@ -130,15 +131,22 @@ class DatabaseRepo(
val entries = localSource.getEntriesList() // catnum -> name
val jobs = Sources.amSatLiveUrls.map { (type, url) ->
async { type to remoteSource.getNetworkStream(url) }
} + async { "Active" to remoteSource.getNetworkStream(Sources.amSatActiveUrl) }
} + async { "Active" to fetchAmSatActiveStream() }
val results = jobs.awaitAll()
val fmNames = results.firstOrNull { it.first == "FM" }?.second?.stream
?.let { dataParser.parseAmSatLivePage(it) }.orEmpty()
val linearNames = results.firstOrNull { it.first == "Linear" }?.second?.stream
?.let { dataParser.parseAmSatLivePage(it) }.orEmpty()
val activeCatnums = results.firstOrNull { it.first == "Active" }?.second?.stream
?.let { dataParser.parseAmSatActiveCatnums(it) }.orEmpty()
settingsRepo.setAmSatActiveCatnums(activeCatnums)
// Amateur whitelist: a failed fetch yields an empty result, which
// must NOT wipe out the last good snapshot. Fall back to the
// persisted whitelist, and only persist when this fetch actually
// returned a stream.
val activeStream = results.firstOrNull { it.first == "Active" }?.second?.stream
val activeCatnums = activeStream
?.let { dataParser.parseAmSatActiveCatnums(it) }
.orEmpty()
.ifEmpty { settingsRepo.getAmSatActiveCatnums() }
if (activeStream != null) settingsRepo.setAmSatActiveCatnums(activeCatnums)
val nameToCatnum = entries.associate { it.name.uppercase() to it.catnum }
// Resolve every matching local entry per AMSAT name. A single match
// is kept as-is (so satellites absent from the amateur whitelist,
@@ -152,10 +160,25 @@ class DatabaseRepo(
}.values.toSet()
if (all.size <= 1) return all
val preferred = all.intersect(activeCatnums)
return if (preferred.isNotEmpty()) preferred else all
if (preferred.isNotEmpty()) return preferred
// Whitelist unavailable: keep only the primary entry (smallest
// catnum — ISS ZARYA=25544 is the smallest of its five module
// 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; 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<Int>): Set<Int> =
previous.mapNotNull { catnum ->
nameToCatnum.entries.firstOrNull { it.value == catnum }?.key
}.flatMap { resolvePerName(it) }.toSet()
val fmCatnums = fmNames.flatMap { resolvePerName(it) }.toSet()
.ifEmpty { cleanStaleList(settingsRepo.getAmSatFmCatnums()) }
val linearCatnums = linearNames.flatMap { resolvePerName(it) }.toSet()
.ifEmpty { cleanStaleList(settingsRepo.getAmSatLinearCatnums()) }
settingsRepo.setAmSatCatnums(fmCatnums, linearCatnums)
println("AMSAT live lists updated: FM=${fmCatnums.size}, Linear=${linearCatnums.size}, Active=${activeCatnums.size}")
}.onFailure {
@@ -164,6 +187,16 @@ class DatabaseRepo(
}
}
/** Try the amateur-whitelist sources in order and return the first
* NetworkResult that carried a stream (or null if all failed). */
private suspend fun fetchAmSatActiveStream(): NetworkResult? {
for (url in Sources.amSatActiveUrls) {
val result = remoteSource.getNetworkStream(url)
if (result.stream != null) return result
}
return null
}
override suspend fun clearAllData() = withContext(dispatcher) {
localSource.deleteEntries()
localSource.deleteRadios()
@@ -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<String> ->
val catnumSet: Set<Int>? = 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<String> ->
val catnumSet: Set<Int>? = 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) }
@@ -97,7 +102,10 @@ class SelectionRepo(
override fun getTypesList() = buildList {
// 三个转发器/活动虚拟类型排在最前.
addAll(Sources.virtualTypeNames)
addAll(Sources.satelliteDataUrls.keys.sorted().toMutableList().apply { removeAt(0) })
// 所有真实 TLE 源类型均可选(含 "All" = CelesTrak active);只排除
// "Other"(空 URL 占位,无数据)。不要用 removeAt(0) —— 字母序第一个
// 是 "All"/"ARISS" 这类真实源,删掉会让用户勾不到它们。
addAll(Sources.satelliteDataUrls.keys.sorted().filterNot { it == "Other" })
}
override suspend fun getEntriesFlow() = withContext(dispatcher) {
@@ -498,7 +498,9 @@ class SettingsRepo(
}
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
if (type == "All") return
// "All" is a real TLE source type (CelesTrak active group) whose ids
// must be persisted like any other type — skipping it made the "All"
// filter resolve to an empty set and show nothing.
val typesString = ids.joinToString(separatorComma)
preferences.edit { putString("type$type", typesString) }
}
@@ -749,6 +751,9 @@ class SettingsRepo(
//endregion
//region # AMSAT live-transponder lists
private val _amSatListsVersion = MutableStateFlow(0)
override val amSatListsVersion: StateFlow<Int> = _amSatListsVersion
override fun getAmSatFmCatnums(): Set<Int> {
val raw = preferences.getString(keyAmSatFmCatnums, null) ?: return emptySet()
return raw.split(separatorComma).mapNotNull { it.toIntOrNull() }.toSet()
@@ -764,6 +769,7 @@ class SettingsRepo(
putString(keyAmSatFmCatnums, fmCatnums.sorted().joinToString(separatorComma))
putString(keyAmSatLinearCatnums, linearCatnums.sorted().joinToString(separatorComma))
}
_amSatListsVersion.update { it + 1 }
}
override fun getAmSatActiveCatnums(): Set<Int> {
@@ -127,6 +127,29 @@ class DatabaseRepoTest {
assertEquals(listOf(98248), settingsRepo.satelliteTypeIdsByType["SatNOGS"])
}
@Test
fun `remote update persists All source type ids`() = runTest(dispatcher) {
// Regression: "All" (CelesTrak active) is a real TLE source type whose
// ids must be persisted on sync — the old `if (type == "All") return`
// silently dropped them, making the "All" filter resolve to nothing.
val allUrl = Sources.satelliteDataUrls.getValue("All")
val localSource = FakeLocalSource()
val remoteSource = FakeRemoteSource().apply {
networkStreams[allUrl] = { validCsvStream() }
}
val settingsRepo = FakeSettingsRepo(
dataSources = DataSourcesSettings(
satelliteUrls = listOf(allUrl),
transceiversUrls = emptyList()
)
)
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
repository.updateFromRemote()
assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["All"])
}
private fun validCsvStream(): InputStream = """
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
@@ -143,6 +166,102 @@ class DatabaseRepoTest {
1 98248U 26237.16675926 .00015724 00000-0 97477-3 0 00013
2 98248 097.5373 310.9694 0011309 278.1232 340.7230 15.09766181000012
""".trimIndent().byteInputStream()
@Test
fun `amsat fm list disambiguates ISS modules via persisted whitelist when active csv fetch fails`() =
runTest(dispatcher) {
val satnogsUrl = Sources.satelliteDataUrls.getValue("SatNOGS")
val localSource = FakeLocalSource()
val remoteSource = FakeRemoteSource().apply {
networkStreams[satnogsUrl] = { issModulesCsvStream() }
networkStreams[Sources.amSatLiveUrls.getValue("FM")] = { amsatFmPageStream() }
// Amateur-whitelist URLs intentionally NOT registered -> fetch fails.
}
val settingsRepo = FakeSettingsRepo(
dataSources = DataSourcesSettings(
satelliteUrls = listOf(satnogsUrl),
transceiversUrls = emptyList()
)
).apply { amSatActive = setOf(25544) } // whitelist persisted by an earlier good sync
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
repository.updateFromRemote()
// Whitelist fetch fails on both sources, but the persisted whitelist
// still disambiguates: only primary ISS (ZARYA) survives,
// ISS (DESTINY) = 26700 must NOT enter the FM list.
assertTrue(25544 in settingsRepo.amSatFm)
assertTrue(26700 !in settingsRepo.amSatFm)
}
@Test
fun `amsat fm list keeps only smallest catnum when whitelist never available`() = runTest(dispatcher) {
val satnogsUrl = Sources.satelliteDataUrls.getValue("SatNOGS")
val localSource = FakeLocalSource()
val remoteSource = FakeRemoteSource().apply {
networkStreams[satnogsUrl] = { issModulesCsvStream() }
networkStreams[Sources.amSatLiveUrls.getValue("FM")] = { amsatFmPageStream() }
}
val settingsRepo = FakeSettingsRepo(
dataSources = DataSourcesSettings(
satelliteUrls = listOf(satnogsUrl),
transceiversUrls = emptyList()
)
) // no persisted whitelist at all
val repository = DatabaseRepo(dispatcher, dataParser, localSource, remoteSource, settingsRepo)
repository.updateFromRemote()
// Without any whitelist, the multi-match collapse keeps the primary
// entry (smallest catnum = ISS ZARYA 25544), never every module alias.
assertEquals(setOf(25544), settingsRepo.amSatFm)
}
@Test
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] = { issModulesCsvStream() }
// FM page NOT registered -> 404 -> empty parse.
}
val settingsRepo = FakeSettingsRepo(
dataSources = DataSourcesSettings(
satelliteUrls = listOf(satnogsUrl),
transceiversUrls = emptyList()
)
).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()
// 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 = """
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
ISS (ZARYA),1998-067A,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25544,999,31220,.31985E-4,.1288E-4,0
ISS (UNITY),1998-067B,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,25575,999,31220,.31985E-4,.1288E-4,0
ISS (ZVEZDA),1998-067C,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,26400,999,31220,.31985E-4,.1288E-4,0
ISS (DESTINY),1998-067D,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,26700,999,31220,.31985E-4,.1288E-4,0
ISS (NAUKA),1998-067E,2021-11-16T12:28:09.322176,15.48582035,.0004694,51.6447,309.4881,203.6966,299.8876,0,U,49044,999,31220,.31985E-4,.1288E-4,0
""".trimIndent().byteInputStream()
private fun amsatFmPageStream(): InputStream = """
<table class="has-background">
<thead><tr><th>Satellite</th><th>Uplink</th><th>Downlink</th><th>Comment</th></tr></thead>
<tbody>
<tr><td>ISS</td><td>145.990 MHz</td><td>437.800 MHz</td><td></td></tr>
<tr><td>AO-91(RadFxSat / Fox-1B)</td><td>435.250 MHz</td><td>145.960 MHz</td><td></td></tr>
</tbody>
</table>
""".trimIndent().byteInputStream()
}
private class FakeRemoteSource : IRemoteSource {
@@ -167,7 +286,8 @@ private class FakeLocalSource : ILocalSource {
override suspend fun getEntriesTotal(): Int = insertedEntries.size
override suspend fun getEntriesList(): List<SatItem> = emptyList()
override suspend fun getEntriesList(): List<SatItem> =
insertedEntries.map { SatItem(it.catnum, it.name) }
override suspend fun getEntriesWithIds(ids: List<Int>): List<OrbitalObject> = emptyList()
@@ -271,11 +391,21 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou
(dataSourcesStatus as? MutableStateFlow)?.value = status
}
override fun getAmSatFmCatnums(): Set<Int> = emptySet()
override fun getAmSatLinearCatnums(): Set<Int> = emptySet()
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) = Unit
override fun getAmSatActiveCatnums(): Set<Int> = emptySet()
override fun setAmSatActiveCatnums(catnums: Set<Int>) = Unit
var amSatFm: Set<Int> = emptySet()
var amSatLinear: Set<Int> = emptySet()
var amSatActive: Set<Int> = emptySet()
override val amSatListsVersion: StateFlow<Int> = MutableStateFlow(0)
override fun getAmSatFmCatnums(): Set<Int> = amSatFm
override fun getAmSatLinearCatnums(): Set<Int> = amSatLinear
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) {
amSatFm = fmCatnums
amSatLinear = linearCatnums
}
override fun getAmSatActiveCatnums(): Set<Int> = amSatActive
override fun setAmSatActiveCatnums(catnums: Set<Int>) {
amSatActive = catnums
}
override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit
@@ -217,6 +217,68 @@ class SelectionRepoSearchTest {
assertTrue("All" in types)
}
@Test
fun `All type resolves to persisted CelesTrak active ids`() = runTest {
// Regression: "All" is a real TLE source type (CelesTrak active). Its
// ids are persisted on sync like any other type; selecting it must
// show the synced satellites, NOT an empty list.
val fake = FakeSettingsRepoForSearch()
fake.setSatelliteTypeIds("All", sampleItems.map { it.catnum })
val repo = SelectionRepo(
dispatcher = Dispatchers.Unconfined,
localSource = FakeLocalSourceForSearch(sampleItems),
settingsRepo = fake
)
repo.setTypes(listOf("All"))
val results = repo.getEntriesFlow().first()
assertEquals(sampleItems.map { it.catnum }.toSet(), results.map { it.catnum }.toSet())
}
@Test
fun `getTypesList excludes only the Other placeholder`() = runTest {
val repo = createRepo(sampleItems)
val types = repo.getTypesList()
// All real TLE source types must be selectable, including "All"
// (CelesTrak active) and "ARISS" (removed by the old removeAt(0)).
assertTrue("All" in types)
assertTrue("ARISS" in types)
assertTrue("R4UAB" in types)
assertTrue("SatNOGS" in types)
assertTrue("Other" !in types)
}
@Test
fun `all types shows all items`() = runTest {
val repo = createRepo(sampleItems)
repo.setTypes(emptyList()) // "All" = no type filtering
val results = repo.getEntriesFlow().first()
assertEquals(sampleItems.size, results.size)
assertEquals(sampleItems.map { it.catnum }.toSet(), results.map { it.catnum }.toSet())
}
@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<SatItem>,
amSatFm: Set<Int> = emptySet(),
@@ -252,10 +314,11 @@ private class FakeLocalSourceForSearch(
}
private class FakeSettingsRepoForSearch(
private val amSatFm: Set<Int> = emptySet(),
private val amSatLinear: Set<Int> = emptySet(),
var amSatFm: Set<Int> = emptySet(),
var amSatLinear: Set<Int> = emptySet(),
private var amSatActive: Set<Int> = emptySet()
) : ISettingsRepo {
private val typeIds = mutableMapOf<String, List<Int>>()
override val appVersionName: String = "test"
override val selectedIds: StateFlow<List<Int>> = MutableStateFlow(emptyList())
override val selectedTypes: StateFlow<List<String>> = MutableStateFlow(emptyList())
@@ -290,8 +353,12 @@ private class FakeSettingsRepoForSearch(
override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean = true
override fun setStationPosition(): Boolean = true
override fun setStationPosition(locator: String): Boolean = true
override fun getSatelliteTypesIds(types: List<String>): List<Int> = emptyList()
override fun setSatelliteTypeIds(type: String, ids: List<Int>) = Unit
override fun getSatelliteTypesIds(types: List<String>): List<Int> =
types.flatMap { typeIds[it].orEmpty() }.distinct()
override fun setSatelliteTypeIds(type: String, ids: List<Int>) {
typeIds[type] = ids
}
override fun updateDatabaseState(state: DatabaseState) = Unit
override fun updateRCSettings(settings: RCSettings) = Unit
override fun updateOtherSettings(transform: (OtherSettings) -> OtherSettings) = Unit
@@ -299,7 +366,12 @@ private class FakeSettingsRepoForSearch(
override fun updateDataSourcesStatus(status: Map<String, Int>) = Unit
override fun getAmSatFmCatnums(): Set<Int> = amSatFm
override fun getAmSatLinearCatnums(): Set<Int> = amSatLinear
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) = Unit
override val amSatListsVersion: StateFlow<Int> = MutableStateFlow(0)
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) {
amSatFm = fmCatnums
amSatLinear = linearCatnums
(amSatListsVersion as MutableStateFlow<Int>).value++
}
override fun getAmSatActiveCatnums(): Set<Int> = amSatActive
override fun setAmSatActiveCatnums(catnums: Set<Int>) { amSatActive = catnums }
override fun updateRadioControlSettings(settings: RadioControlSettings) = Unit
@@ -89,6 +89,11 @@ interface ISettingsRepo {
* active-transponder table (whitelist for the virtual type filters). */
fun getAmSatActiveCatnums(): Set<Int>
fun setAmSatActiveCatnums(catnums: Set<Int>)
/** 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<Int>
//endregion
//region # Radio control settings
@@ -68,9 +68,14 @@ object Sources {
* transponder with its NORAD catnum. Used as the authoritative whitelist
* of *amateur* satellites, so the virtual filters never match
* non-amateur debris (Ariane 6 R/B), retired weather sats (TIROS), or
* ISS station-module aliases (ISS (DESTINY) etc.). */
const val amSatActiveUrl =
"https://raw.githubusercontent.com/palewire/amateur-satellite-database/main/data/amsat-active-frequencies.csv"
* ISS station-module aliases (ISS (DESTINY) etc.). Primary source plus
* a jsDelivr CDN mirror: raw.githubusercontent.com is unreachable on
* some networks (e.g. mainland China mobile), and a failed whitelist
* fetch would silently disable the module disambiguation. */
val amSatActiveUrls = listOf(
"https://raw.githubusercontent.com/palewire/amateur-satellite-database/main/data/amsat-active-frequencies.csv",
"https://cdn.jsdelivr.net/gh/palewire/amateur-satellite-database@main/data/amsat-active-frequencies.csv"
)
/** Virtual satellite-selection types: transponder/activity filters shown
* at the top of the type picker. They resolve to live lists (AMSAT pages
@@ -285,6 +285,7 @@ class LoTWGridSyncTest {
override fun updateDataSourcesStatus(status: Map<String, Int>) = TODO()
override fun getAmSatFmCatnums(): Set<Int> = emptySet()
override fun getAmSatLinearCatnums(): Set<Int> = emptySet()
override val amSatListsVersion: StateFlow<Int> = MutableStateFlow(0)
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) = Unit
override fun getAmSatActiveCatnums(): Set<Int> = emptySet()
override fun setAmSatActiveCatnums(catnums: Set<Int>) = Unit
@@ -112,6 +112,7 @@ class FakeSettingsRepo(
override fun getAmSatFmCatnums(): Set<Int> = amSatFm
override fun getAmSatLinearCatnums(): Set<Int> = amSatLinear
override val amSatListsVersion: StateFlow<Int> = MutableStateFlow(0)
override fun setAmSatCatnums(fmCatnums: Set<Int>, linearCatnums: Set<Int>) = TODO()
override fun getAmSatActiveCatnums(): Set<Int> = emptySet()
override fun setAmSatActiveCatnums(catnums: Set<Int>) = TODO()
+2 -2
View File
@@ -1,8 +1,8 @@
[versions]
#noinspection UnusedVersionCatalogEntry
appVersionCode = "500"
appVersionCode = "503"
#noinspection UnusedVersionCatalogEntry
appVersionName = "4.4.7-ba7opf.15.3"
appVersionName = "4.4.7-ba7opf.15.6"
#noinspection UnusedVersionCatalogEntry
compileSdk = "37"
#noinspection UnusedVersionCatalogEntry