mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-05 03:57:24 +00:00
feat(lotw): auto-sync grids on app start with separate toggle
Mirrors the ephemeris auto-update check: on every app start, if LoTW credentials are configured, the new 'auto-sync LoTW grids' toggle is on, at least one manual sync was done before, and today has not synced yet (ARRL rate limit, ~once a day), pull confirmations since the last sync and merge them. Same callsign -> incremental merge; callsign change -> full replace. Manual dialog and automatic path share the orchestration (mode resolution, gate, merge/persist) moved to core:domain LoTWGridSync.kt; LoTWSyncMode moved to core:domain. 8 unit tests.
This commit is contained in:
1 parent
5f1eb239ed
commit
60dfa5d600
17 files changed
+466
-61
No files matched your search
@@ -24,6 +24,12 @@ import android.os.Bundle
|
|||||||
import com.rtbishop.look4sat.core.data.injection.MainContainer
|
import com.rtbishop.look4sat.core.data.injection.MainContainer
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.LoTWResult
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.applyLoTWGridResult
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.lotwSyncToday
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.resolveLoTWSyncMode
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.shouldAutoSyncLoTW
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -41,6 +47,8 @@ class MainApplication : Application(), IContainerProvider {
|
|||||||
clearAmSatCacheOnAppBackground()
|
clearAmSatCacheOnAppBackground()
|
||||||
// trigger automatic update every 48 hours
|
// trigger automatic update every 48 hours
|
||||||
container.appScope.launch { checkAutoUpdate() }
|
container.appScope.launch { checkAutoUpdate() }
|
||||||
|
// automatic LoTW grid sync on every app start (gated, see checkLoTWAutoSync)
|
||||||
|
container.appScope.launch { checkLoTWAutoSync() }
|
||||||
// load satellite data on every app start
|
// load satellite data on every app start
|
||||||
container.appScope.launch { container.satelliteRepo.initRepository() }
|
container.appScope.launch { container.satelliteRepo.initRepository() }
|
||||||
}
|
}
|
||||||
@@ -79,4 +87,37 @@ class MainApplication : Application(), IContainerProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatic LoTW grid sync, mirroring [checkAutoUpdate]: checked on every
|
||||||
|
* app start, but only pulls when due — LoTW credentials configured, the
|
||||||
|
* LoTW auto-sync toggle on, at least one prior (manual) sync, and not
|
||||||
|
* already synced today (ARRL limits report pulls, ~once a day per account).
|
||||||
|
* Incremental when the callsign is unchanged; a callsign change falls back
|
||||||
|
* to a full replace so no stale grids from another account linger.
|
||||||
|
* Failures are silent and simply retried on the next app start — the manual
|
||||||
|
* sync button still surfaces the explicit cause to the user.
|
||||||
|
*/
|
||||||
|
private suspend fun checkLoTWAutoSync(timeNow: Long = System.currentTimeMillis()) {
|
||||||
|
val settingsRepo = container.settingsRepo
|
||||||
|
val lotwSettings = settingsRepo.lotwSettings.value
|
||||||
|
if (!shouldAutoSyncLoTW(
|
||||||
|
isConfigured = lotwSettings.isConfigured,
|
||||||
|
autoLotwSyncEnabled = settingsRepo.otherSettings.value.stateOfAutoLotwSync,
|
||||||
|
lastSyncDate = settingsRepo.getLastLotwSyncDate(),
|
||||||
|
today = lotwSyncToday(timeNow)
|
||||||
|
)
|
||||||
|
) return
|
||||||
|
val callsign = lotwSettings.callsign.trim().uppercase()
|
||||||
|
val mode = resolveLoTWSyncMode(settingsRepo.getLastLotwSyncCallsign(), callsign, requested = null)
|
||||||
|
val since = if (mode == LoTWSyncMode.Incremental) settingsRepo.getLastLotwSyncDate() else ""
|
||||||
|
println("Started periodic LoTW grid sync (${mode.name.lowercase()})")
|
||||||
|
val result = container.lotwRepo.fetchConfirmedGridQsos(callsign, lotwSettings.password, since)
|
||||||
|
if (result is LoTWResult.Success) {
|
||||||
|
val count = applyLoTWGridResult(settingsRepo, result, mode, callsign, timeNow)
|
||||||
|
println("Periodic LoTW grid sync finished: $count worked grids")
|
||||||
|
} else {
|
||||||
|
println("Periodic LoTW grid sync skipped (${result::class.simpleName})")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -77,6 +77,7 @@ class SettingsRepo(
|
|||||||
private val keySelectedTypes = "selectedTypes"
|
private val keySelectedTypes = "selectedTypes"
|
||||||
private val keySelectedModes = "selectedModes"
|
private val keySelectedModes = "selectedModes"
|
||||||
private val keyStateOfAutoUpdate = "stateOfAutoUpdate"
|
private val keyStateOfAutoUpdate = "stateOfAutoUpdate"
|
||||||
|
private val keyStateOfAutoLotwSync = "stateOfAutoLotwSync"
|
||||||
private val keyStateOfSensors = "stateOfSensors"
|
private val keyStateOfSensors = "stateOfSensors"
|
||||||
private val keyStateOfSweep = "stateOfSweep"
|
private val keyStateOfSweep = "stateOfSweep"
|
||||||
private val keyStateOfUtc = "stateOfUtc"
|
private val keyStateOfUtc = "stateOfUtc"
|
||||||
@@ -538,6 +539,7 @@ class SettingsRepo(
|
|||||||
val new = transform(current)
|
val new = transform(current)
|
||||||
preferences.edit {
|
preferences.edit {
|
||||||
putBoolean(keyStateOfAutoUpdate, new.stateOfAutoUpdate)
|
putBoolean(keyStateOfAutoUpdate, new.stateOfAutoUpdate)
|
||||||
|
putBoolean(keyStateOfAutoLotwSync, new.stateOfAutoLotwSync)
|
||||||
putBoolean(keyStateOfSensors, new.stateOfSensors)
|
putBoolean(keyStateOfSensors, new.stateOfSensors)
|
||||||
putBoolean(keyStateOfSweep, new.stateOfSweep)
|
putBoolean(keyStateOfSweep, new.stateOfSweep)
|
||||||
putBoolean(keyStateOfUtc, new.stateOfUtc)
|
putBoolean(keyStateOfUtc, new.stateOfUtc)
|
||||||
@@ -556,6 +558,7 @@ class SettingsRepo(
|
|||||||
|
|
||||||
private fun getOtherSettings(): OtherSettings = OtherSettings(
|
private fun getOtherSettings(): OtherSettings = OtherSettings(
|
||||||
stateOfAutoUpdate = preferences.getBoolean(keyStateOfAutoUpdate, true),
|
stateOfAutoUpdate = preferences.getBoolean(keyStateOfAutoUpdate, true),
|
||||||
|
stateOfAutoLotwSync = preferences.getBoolean(keyStateOfAutoLotwSync, true),
|
||||||
stateOfSensors = preferences.getBoolean(keyStateOfSensors, true),
|
stateOfSensors = preferences.getBoolean(keyStateOfSensors, true),
|
||||||
stateOfSweep = preferences.getBoolean(keyStateOfSweep, true),
|
stateOfSweep = preferences.getBoolean(keyStateOfSweep, true),
|
||||||
stateOfUtc = preferences.getBoolean(keyStateOfUtc, false),
|
stateOfUtc = preferences.getBoolean(keyStateOfUtc, false),
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ data class OtherSettings(
|
|||||||
val shouldSeeWhatsNew: Boolean,
|
val shouldSeeWhatsNew: Boolean,
|
||||||
val sstvMode: String = "Auto",
|
val sstvMode: String = "Auto",
|
||||||
val lowElevation: Double = 15.0,
|
val lowElevation: Double = 15.0,
|
||||||
val highElevation: Double = 45.0
|
val highElevation: Double = 45.0,
|
||||||
|
/** Independent auto-sync toggle for LoTW confirmed grids (separate from the
|
||||||
|
* ephemeris auto-update; only effective after at least one manual sync). */
|
||||||
|
val stateOfAutoLotwSync: Boolean = true
|
||||||
)
|
)
|
||||||
|
|
||||||
data class DataSourcesSettings(
|
data class DataSourcesSettings(
|
||||||
|
|||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||||
|
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.rtbishop.look4sat.core.domain.repository
|
||||||
|
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.GridQso
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared LoTW sync orchestration used by BOTH the manual sync (settings screen)
|
||||||
|
* and the automatic sync on app start (MainApplication), so the two paths can
|
||||||
|
* never drift apart:
|
||||||
|
*
|
||||||
|
* - [LoTWSyncMode.Incremental] pulls only the confirmations since the last sync
|
||||||
|
* and MERGES them into the stored grids/QSOs (grids only grow; QSO detail is
|
||||||
|
* deduped by call + QSO time).
|
||||||
|
* - [LoTWSyncMode.Full] pulls everything and REPLACES the stored data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** UTC date ("yyyyMMdd") of the given instant — the LoTW sync cursor granularity. */
|
||||||
|
fun lotwSyncToday(now: Long = System.currentTimeMillis()): String =
|
||||||
|
SimpleDateFormat("yyyyMMdd", Locale.US)
|
||||||
|
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||||
|
.format(Date(now))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the effective sync mode for a given request:
|
||||||
|
* - an explicit [LoTWSyncMode.Full] request is always honored;
|
||||||
|
* - an [LoTWSyncMode.Incremental] request is honored only when the callsign
|
||||||
|
* matches the last sync — a first sync, an unknown/empty record or a
|
||||||
|
* callsign change falls back to [LoTWSyncMode.Full] so no stale grids from
|
||||||
|
* another account linger.
|
||||||
|
* With [requested] == null (automatic sync) it picks incremental whenever
|
||||||
|
* possible and full otherwise.
|
||||||
|
*/
|
||||||
|
fun resolveLoTWSyncMode(
|
||||||
|
lastSyncCallsign: String,
|
||||||
|
callsign: String,
|
||||||
|
requested: LoTWSyncMode?
|
||||||
|
): LoTWSyncMode {
|
||||||
|
if (requested == LoTWSyncMode.Full) return LoTWSyncMode.Full
|
||||||
|
val sameCall = lastSyncCallsign.isNotBlank() && lastSyncCallsign == callsign
|
||||||
|
return if (sameCall) LoTWSyncMode.Incremental else LoTWSyncMode.Full
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the automatic LoTW sync should run now — mirrors the ephemeris
|
||||||
|
* auto-update check: credentials configured, the LoTW auto-sync toggle on, at
|
||||||
|
* least one prior manual sync (the first sync stays manual/full), and not
|
||||||
|
* already synced today (ARRL limits report pulls, ~once a day per account).
|
||||||
|
*/
|
||||||
|
fun shouldAutoSyncLoTW(
|
||||||
|
isConfigured: Boolean,
|
||||||
|
autoLotwSyncEnabled: Boolean,
|
||||||
|
lastSyncDate: String,
|
||||||
|
today: String
|
||||||
|
): Boolean = isConfigured && autoLotwSyncEnabled && lastSyncDate.isNotBlank() && lastSyncDate != today
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a successful LoTW report to the stored grid data and advances the
|
||||||
|
* sync cursor so the next incremental pull asks LoTW for only the
|
||||||
|
* confirmations since today. Persistence runs on the IO dispatcher — building
|
||||||
|
* the per-grid QSO JSON and writing SharedPreferences blocks the calling
|
||||||
|
* thread, and for large accounts (multi-MB JSON) that froze the main thread
|
||||||
|
* after a manual sync ('bar done but app stuck').
|
||||||
|
*
|
||||||
|
* @return the resulting number of worked grids.
|
||||||
|
*/
|
||||||
|
suspend fun applyLoTWGridResult(
|
||||||
|
settingsRepo: ISettingsRepo,
|
||||||
|
result: LoTWResult.Success,
|
||||||
|
mode: LoTWSyncMode,
|
||||||
|
callsign: String,
|
||||||
|
now: Long = System.currentTimeMillis()
|
||||||
|
): Int {
|
||||||
|
val existingGrids = settingsRepo.getWorkedGrids()
|
||||||
|
val existingQsos = settingsRepo.getWorkedGridQsos()
|
||||||
|
val existingRoamed = settingsRepo.getRoamedGrids()
|
||||||
|
val mergedGrids = if (mode == LoTWSyncMode.Incremental) existingGrids + result.grids else result.grids
|
||||||
|
val mergedQsos = if (mode == LoTWSyncMode.Incremental) mergeGridQsos(existingQsos, result.qsos) else result.qsos
|
||||||
|
val mergedRoamed = if (mode == LoTWSyncMode.Incremental) existingRoamed + result.roamedGrids else result.roamedGrids
|
||||||
|
settingsRepo.setLastLotwSyncDate(lotwSyncToday(now))
|
||||||
|
settingsRepo.setLastLotwSyncCallsign(callsign)
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
settingsRepo.setWorkedGrids(mergedGrids)
|
||||||
|
settingsRepo.setWorkedGridQsos(mergedQsos)
|
||||||
|
settingsRepo.setRoamedGrids(mergedRoamed)
|
||||||
|
}
|
||||||
|
return mergedGrids.size
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Merge fresh QSO detail into existing per-grid lists, dedup by call + QSO time. */
|
||||||
|
fun mergeGridQsos(
|
||||||
|
existing: Map<String, List<GridQso>>,
|
||||||
|
fresh: Map<String, List<GridQso>>
|
||||||
|
): Map<String, List<GridQso>> {
|
||||||
|
val merged = existing.mapValues { (_, list) -> list.toMutableList() }.toMutableMap()
|
||||||
|
fresh.forEach { (grid, list) ->
|
||||||
|
val target = merged.getOrPut(grid) { mutableListOf() }
|
||||||
|
val known = target.mapTo(mutableSetOf()) { it.call to it.epochMs }
|
||||||
|
list.forEach { qso ->
|
||||||
|
if (known.add(qso.call to qso.epochMs)) target.add(qso)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
@@ -46,3 +46,6 @@ sealed class LoTWResult {
|
|||||||
/** Any other network-level failure (DNS, TLS, connection reset, HTTP 5xx). */
|
/** Any other network-level failure (DNS, TLS, connection reset, HTTP 5xx). */
|
||||||
data class NetworkError(val detail: String) : LoTWResult()
|
data class NetworkError(val detail: String) : LoTWResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** What a LoTW sync does: pull everything, or only new QSLs since the last sync. */
|
||||||
|
enum class LoTWSyncMode { Full, Incremental }
|
||||||
+261
@@ -0,0 +1,261 @@
|
|||||||
|
/*
|
||||||
|
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||||
|
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.rtbishop.look4sat.core.domain.repository
|
||||||
|
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.DatabaseState
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.GridQso
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.LoTWSettings
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.OtherSettings
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.PassesSettings
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||||
|
import com.rtbishop.look4sat.core.domain.model.WavelogSettings
|
||||||
|
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class LoTWGridSyncTest {
|
||||||
|
|
||||||
|
// 2026-09-16T00:00:00Z — fixed "now" for the cursor assertions.
|
||||||
|
private val now = 1_789_516_800_000L
|
||||||
|
|
||||||
|
// region resolveLoTWSyncMode
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun resolveModeHonorsExplicitFull() {
|
||||||
|
assertEquals(LoTWSyncMode.Full, resolveLoTWSyncMode("BA7OPF", "BA7OPF", LoTWSyncMode.Full))
|
||||||
|
assertEquals(LoTWSyncMode.Full, resolveLoTWSyncMode("", "BA7OPF", LoTWSyncMode.Full))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun resolveModeAllowsIncrementalOnlyForSameCallsign() {
|
||||||
|
assertEquals(
|
||||||
|
LoTWSyncMode.Incremental,
|
||||||
|
resolveLoTWSyncMode("BA7OPF", "BA7OPF", LoTWSyncMode.Incremental)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
LoTWSyncMode.Full,
|
||||||
|
resolveLoTWSyncMode("BG7ABC", "BA7OPF", LoTWSyncMode.Incremental)
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
LoTWSyncMode.Full,
|
||||||
|
resolveLoTWSyncMode("", "BA7OPF", LoTWSyncMode.Incremental)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun resolveModeForAutoSyncPicksIncrementalWhenPossible() {
|
||||||
|
assertEquals(LoTWSyncMode.Incremental, resolveLoTWSyncMode("BA7OPF", "BA7OPF", null))
|
||||||
|
assertEquals(LoTWSyncMode.Full, resolveLoTWSyncMode("BG7ABC", "BA7OPF", null))
|
||||||
|
assertEquals(LoTWSyncMode.Full, resolveLoTWSyncMode("", "BA7OPF", null))
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region shouldAutoSyncLoTW
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun autoSyncGateRequiresEverything() {
|
||||||
|
val today = "20260916"
|
||||||
|
// Not configured.
|
||||||
|
assertFalse(shouldAutoSyncLoTW(false, true, "20260915", today))
|
||||||
|
// Auto-update toggle off.
|
||||||
|
assertFalse(shouldAutoSyncLoTW(true, false, "20260915", today))
|
||||||
|
// Never synced manually — first sync stays manual/full.
|
||||||
|
assertFalse(shouldAutoSyncLoTW(true, true, "", today))
|
||||||
|
// Already synced today — ARRL rate-limit guard.
|
||||||
|
assertFalse(shouldAutoSyncLoTW(true, true, today, today))
|
||||||
|
// All gates open.
|
||||||
|
assertTrue(shouldAutoSyncLoTW(true, true, "20260915", today))
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region mergeGridQsos
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mergeGridQsosDedupsByCallAndTime() {
|
||||||
|
val existing = mapOf(
|
||||||
|
"OL62" to listOf(qso("BA7ABC", 1000L)),
|
||||||
|
"OL63" to listOf(qso("BA7DEF", 2000L))
|
||||||
|
)
|
||||||
|
val fresh = mapOf(
|
||||||
|
// Same grid, same QSO re-delivered by an overlapping since-date.
|
||||||
|
"OL62" to listOf(qso("BA7ABC", 1000L)),
|
||||||
|
// Same grid, brand-new QSO.
|
||||||
|
"OL62" to listOf(qso("BA7GHI", 3000L)),
|
||||||
|
// New grid entirely.
|
||||||
|
"PM95" to listOf(qso("BA7JKL", 4000L))
|
||||||
|
)
|
||||||
|
val merged = mergeGridQsos(existing, fresh)
|
||||||
|
assertEquals(setOf("OL62", "OL63", "PM95"), merged.keys)
|
||||||
|
assertEquals(2, merged["OL62"]?.size) // BA7ABC once, BA7GHI added
|
||||||
|
assertEquals(1, merged["OL63"]?.size)
|
||||||
|
assertEquals(1, merged["PM95"]?.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mergeGridQsosHandlesEmptySides() {
|
||||||
|
val fresh = mapOf("OL62" to listOf(qso("BA7ABC", 1000L)))
|
||||||
|
assertEquals(fresh, mergeGridQsos(emptyMap(), fresh))
|
||||||
|
// An empty incremental pull must leave the stored side untouched.
|
||||||
|
assertEquals(fresh, mergeGridQsos(fresh, emptyMap()))
|
||||||
|
assertEquals(emptyMap<String, List<GridQso>>(), mergeGridQsos(emptyMap(), emptyMap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region applyLoTWGridResult
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun applyIncrementalMergesAndAdvancesCursor() = runBlocking {
|
||||||
|
val repo = FakeSettingsRepo().apply {
|
||||||
|
setWorkedGrids(setOf("OL62"))
|
||||||
|
setWorkedGridQsos(mapOf("OL62" to listOf(qso("BA7ABC", 1000L))))
|
||||||
|
setRoamedGrids(setOf("OL62"))
|
||||||
|
setLastLotwSyncDate("20260915")
|
||||||
|
setLastLotwSyncCallsign("BA7OPF")
|
||||||
|
}
|
||||||
|
val result = LoTWResult.Success(
|
||||||
|
grids = setOf("OL62", "PM95"),
|
||||||
|
qsos = mapOf("OL62" to listOf(qso("BA7ABC", 1000L)), "PM95" to listOf(qso("BA7GHI", 2000L))),
|
||||||
|
roamedGrids = setOf("PM95")
|
||||||
|
)
|
||||||
|
val count = applyLoTWGridResult(repo, result, LoTWSyncMode.Incremental, "BA7OPF", now)
|
||||||
|
|
||||||
|
assertEquals(2, count)
|
||||||
|
assertEquals(setOf("OL62", "PM95"), repo.getWorkedGrids())
|
||||||
|
assertEquals(1, repo.getWorkedGridQsos()["OL62"]?.size) // deduped
|
||||||
|
assertEquals(setOf("OL62", "PM95"), repo.getRoamedGrids())
|
||||||
|
assertEquals("20260916", repo.getLastLotwSyncDate())
|
||||||
|
assertEquals("BA7OPF", repo.getLastLotwSyncCallsign())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun applyFullReplacesEverythingAndAdvancesCursor() = runBlocking {
|
||||||
|
val repo = FakeSettingsRepo().apply {
|
||||||
|
setWorkedGrids(setOf("OL62", "STALE"))
|
||||||
|
setWorkedGridQsos(mapOf("STALE" to listOf(qso("BG7OLD", 1L))))
|
||||||
|
setRoamedGrids(setOf("STALE"))
|
||||||
|
setLastLotwSyncDate("20260910")
|
||||||
|
setLastLotwSyncCallsign("BG7OLD")
|
||||||
|
}
|
||||||
|
val result = LoTWResult.Success(
|
||||||
|
grids = setOf("PM95"),
|
||||||
|
qsos = mapOf("PM95" to listOf(qso("BA7GHI", 2000L))),
|
||||||
|
roamedGrids = setOf("PM95")
|
||||||
|
)
|
||||||
|
val count = applyLoTWGridResult(repo, result, LoTWSyncMode.Full, "BA7OPF", now)
|
||||||
|
|
||||||
|
assertEquals(1, count)
|
||||||
|
assertEquals(setOf("PM95"), repo.getWorkedGrids())
|
||||||
|
assertEquals(setOf("PM95"), repo.getWorkedGridQsos().keys)
|
||||||
|
assertEquals(setOf("PM95"), repo.getRoamedGrids())
|
||||||
|
assertEquals("20260916", repo.getLastLotwSyncDate())
|
||||||
|
assertEquals("BA7OPF", repo.getLastLotwSyncCallsign())
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
private fun qso(call: String, epochMs: Long): GridQso =
|
||||||
|
GridQso(call, epochMs, "FO-29", "FM", "70CM", "2M")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal ISettingsRepo fake backing only the LoTW sync surface; the rest
|
||||||
|
* throws (mirrors the FakeSettingsRepo pattern used in feature tests).
|
||||||
|
*/
|
||||||
|
private class FakeSettingsRepo : ISettingsRepo {
|
||||||
|
override val stationPosition: StateFlow<GeoPos> = MutableStateFlow(GeoPos(23.13, 113.26))
|
||||||
|
override val appVersionName: String = "test"
|
||||||
|
override val selectedIds: StateFlow<List<Int>> = MutableStateFlow(emptyList())
|
||||||
|
override val selectedTypes: StateFlow<List<String>> = MutableStateFlow(emptyList())
|
||||||
|
override val passesSettings: StateFlow<PassesSettings> = MutableStateFlow(
|
||||||
|
PassesSettings(hoursAhead = 24, minElevation = 10.0, selectedModes = emptyList())
|
||||||
|
)
|
||||||
|
override val databaseState: StateFlow<DatabaseState> = MutableStateFlow(DatabaseState(0, 0, 0L))
|
||||||
|
override val rcSettings: StateFlow<RCSettings> = MutableStateFlow(
|
||||||
|
RCSettings(false, "", "", "", false, "", "", "", 0L, false, "", "", "", false, "", "")
|
||||||
|
)
|
||||||
|
override val otherSettings: StateFlow<OtherSettings> = MutableStateFlow(
|
||||||
|
OtherSettings(
|
||||||
|
stateOfAutoUpdate = true,
|
||||||
|
stateOfSensors = false,
|
||||||
|
stateOfSweep = false,
|
||||||
|
stateOfUtc = false,
|
||||||
|
stateOfLightTheme = false,
|
||||||
|
stateOfNightMode = false,
|
||||||
|
stateOfMapGrid = false,
|
||||||
|
shouldSeeWarning = false,
|
||||||
|
shouldSeeWhatsNew = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
override val dataSourcesSettings: StateFlow<DataSourcesSettings> = MutableStateFlow(
|
||||||
|
DataSourcesSettings(satelliteUrls = emptyList(), transceiversUrls = emptyList())
|
||||||
|
)
|
||||||
|
override val dataSourcesStatus: StateFlow<Map<String, Int>> = MutableStateFlow(emptyMap())
|
||||||
|
override val radioControlSettings: StateFlow<RadioControlSettings> = MutableStateFlow(
|
||||||
|
RadioControlSettings(false, RadioControlSettings.MODEL_YAESU_FT817, "", "", "", "", 9600)
|
||||||
|
)
|
||||||
|
override val wavelogSettings: StateFlow<WavelogSettings> = MutableStateFlow(WavelogSettings())
|
||||||
|
override val lotwSettings: StateFlow<LoTWSettings> = MutableStateFlow(LoTWSettings())
|
||||||
|
|
||||||
|
private var workedGrids: Set<String> = emptySet()
|
||||||
|
private var workedGridQsos: Map<String, List<GridQso>> = emptyMap()
|
||||||
|
private var roamedGrids: Set<String> = emptySet()
|
||||||
|
private var lastSyncDate: String = ""
|
||||||
|
private var lastSyncCallsign: String = ""
|
||||||
|
|
||||||
|
override fun getWorkedGrids(): Set<String> = workedGrids
|
||||||
|
override fun setWorkedGrids(grids: Set<String>) { workedGrids = grids }
|
||||||
|
override fun getWorkedGridQsos(): Map<String, List<GridQso>> = workedGridQsos
|
||||||
|
override fun setWorkedGridQsos(qsos: Map<String, List<GridQso>>) { workedGridQsos = qsos }
|
||||||
|
override fun getRoamedGrids(): Set<String> = roamedGrids
|
||||||
|
override fun setRoamedGrids(grids: Set<String>) { roamedGrids = grids }
|
||||||
|
override fun getLastLotwSyncDate(): String = lastSyncDate
|
||||||
|
override fun setLastLotwSyncDate(date: String) { lastSyncDate = date }
|
||||||
|
override fun getLastLotwSyncCallsign(): String = lastSyncCallsign
|
||||||
|
override fun setLastLotwSyncCallsign(callsign: String) { lastSyncCallsign = callsign }
|
||||||
|
|
||||||
|
override fun setSelectedIds(ids: List<Int>) = TODO()
|
||||||
|
override fun setSelectedTypes(types: List<String>) = TODO()
|
||||||
|
override fun setPassesSettings(settings: PassesSettings) = TODO()
|
||||||
|
override fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean = TODO()
|
||||||
|
override fun setStationPosition(): Boolean = TODO()
|
||||||
|
override fun setStationPosition(locator: String): Boolean = TODO()
|
||||||
|
override fun getSatelliteTypesIds(types: List<String>): List<Int> = TODO()
|
||||||
|
override fun setSatelliteTypeIds(type: String, ids: List<Int>) = TODO()
|
||||||
|
override fun updateDatabaseState(state: DatabaseState) = TODO()
|
||||||
|
override fun updateRCSettings(settings: RCSettings) = TODO()
|
||||||
|
override fun updateOtherSettings(transform: (OtherSettings) -> OtherSettings) = TODO()
|
||||||
|
override fun updateDataSourcesSettings(settings: DataSourcesSettings) = TODO()
|
||||||
|
override fun updateDataSourcesStatus(status: Map<String, Int>) = TODO()
|
||||||
|
override fun updateRadioControlSettings(settings: RadioControlSettings) = TODO()
|
||||||
|
override fun getSatelliteOffset(catnum: Int): String = ""
|
||||||
|
override fun setSatelliteOffset(catnum: Int, offset: String) = TODO()
|
||||||
|
override fun getAmSatCallsign(): String = ""
|
||||||
|
override fun setAmSatCallsign(callsign: String) = TODO()
|
||||||
|
override fun updateWavelogSettings(settings: WavelogSettings) = TODO()
|
||||||
|
override fun updateLoTWSettings(settings: LoTWSettings) = TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -147,6 +147,7 @@
|
|||||||
<string name="prefs_other_title">Otros ajustes</string>
|
<string name="prefs_other_title">Otros ajustes</string>
|
||||||
<string name="prefs_other_switch_utc">Mostrar tiempos de pase en UTC</string>
|
<string name="prefs_other_switch_utc">Mostrar tiempos de pase en UTC</string>
|
||||||
<string name="prefs_other_switch_update">Habilitar auto actualización de datos</string>
|
<string name="prefs_other_switch_update">Habilitar auto actualización de datos</string>
|
||||||
|
<string name="prefs_other_switch_lotw_sync">Sincronizar automáticamente las cuadrículas LoTW confirmadas</string>
|
||||||
<string name="prefs_other_switch_sweep">Habilitar animaciones radar</string>
|
<string name="prefs_other_switch_sweep">Habilitar animaciones radar</string>
|
||||||
<string name="prefs_other_switch_sensors">Usar sensores para rotar vista de radar</string>
|
<string name="prefs_other_switch_sensors">Usar sensores para rotar vista de radar</string>
|
||||||
<string name="prefs_other_switch_night_mode">Activar filtro nocturno rojo</string>
|
<string name="prefs_other_switch_night_mode">Activar filtro nocturno rojo</string>
|
||||||
|
|||||||
@@ -146,6 +146,7 @@
|
|||||||
<string name="prefs_other_title">Другие настройки</string>
|
<string name="prefs_other_title">Другие настройки</string>
|
||||||
<string name="prefs_other_switch_utc">Показывать время по UTC</string>
|
<string name="prefs_other_switch_utc">Показывать время по UTC</string>
|
||||||
<string name="prefs_other_switch_update">Обновлять данные автоматически</string>
|
<string name="prefs_other_switch_update">Обновлять данные автоматически</string>
|
||||||
|
<string name="prefs_other_switch_lotw_sync">Автосинхронизация подтверждённых сеток LoTW</string>
|
||||||
<string name="prefs_other_switch_sweep">Показывать анимацию радара</string>
|
<string name="prefs_other_switch_sweep">Показывать анимацию радара</string>
|
||||||
<string name="prefs_other_switch_sensors">Использовать сенсоры устройства</string>
|
<string name="prefs_other_switch_sensors">Использовать сенсоры устройства</string>
|
||||||
<string name="prefs_other_switch_night_mode">Включить красный ночной фильтр</string>
|
<string name="prefs_other_switch_night_mode">Включить красный ночной фильтр</string>
|
||||||
|
|||||||
@@ -147,6 +147,7 @@
|
|||||||
<string name="prefs_other_title">වෙනත් සැකසුම්</string>
|
<string name="prefs_other_title">වෙනත් සැකසුම්</string>
|
||||||
<string name="prefs_other_switch_utc">පසුකර වේලාවන් UTC මගින්</string>
|
<string name="prefs_other_switch_utc">පසුකර වේලාවන් UTC මගින්</string>
|
||||||
<string name="prefs_other_switch_update">ස්වයං දත්ත යාවත්කාලීනය</string>
|
<string name="prefs_other_switch_update">ස්වයං දත්ත යාවත්කාලීනය</string>
|
||||||
|
<string name="prefs_other_switch_lotw_sync">LoTW තහවුරු කළ ජාල ස්වයං සමමුහුර්තකරණය</string>
|
||||||
<string name="prefs_other_switch_sweep">radar sweep සජීවිකරණය සබල කරන්න</string>
|
<string name="prefs_other_switch_sweep">radar sweep සජීවිකරණය සබල කරන්න</string>
|
||||||
<string name="prefs_other_switch_sensors">Radar දර්ශනය කරකැවීමට සංවේදක භාවිතා කරන්න </string>
|
<string name="prefs_other_switch_sensors">Radar දර්ශනය කරකැවීමට සංවේදක භාවිතා කරන්න </string>
|
||||||
<string name="prefs_other_switch_night_mode">රතු රාත්රී පෙරහන සබල කරන්න</string>
|
<string name="prefs_other_switch_night_mode">රතු රාත්රී පෙරහන සබල කරන්න</string>
|
||||||
|
|||||||
@@ -189,6 +189,7 @@
|
|||||||
<string name="prefs_other_title">Diğer ayarlar</string>
|
<string name="prefs_other_title">Diğer ayarlar</string>
|
||||||
<string name="prefs_other_switch_utc">Geçiş saatini UTC olarak göster</string>
|
<string name="prefs_other_switch_utc">Geçiş saatini UTC olarak göster</string>
|
||||||
<string name="prefs_other_switch_update">Otomatik veri güncellemeyi etkinleştir</string>
|
<string name="prefs_other_switch_update">Otomatik veri güncellemeyi etkinleştir</string>
|
||||||
|
<string name="prefs_other_switch_lotw_sync">LoTW onaylı kareleri otomatik senkronize et</string>
|
||||||
<string name="prefs_other_switch_sweep">Radar taramasını etkinleştir</string>
|
<string name="prefs_other_switch_sweep">Radar taramasını etkinleştir</string>
|
||||||
<string name="prefs_other_switch_sensors">Radar görünümünü döndürmek için sensörleri kullan</string>
|
<string name="prefs_other_switch_sensors">Radar görünümünü döndürmek için sensörleri kullan</string>
|
||||||
<string name="prefs_other_switch_night_mode">Kırmızı gece filtresini etkinleştir</string>
|
<string name="prefs_other_switch_night_mode">Kırmızı gece filtresini etkinleştir</string>
|
||||||
|
|||||||
@@ -147,6 +147,7 @@
|
|||||||
<string name="prefs_other_title">Інші налаштування</string>
|
<string name="prefs_other_title">Інші налаштування</string>
|
||||||
<string name="prefs_other_switch_utc">Показувати час в UTC</string>
|
<string name="prefs_other_switch_utc">Показувати час в UTC</string>
|
||||||
<string name="prefs_other_switch_update">Автоматичне оновлення даних</string>
|
<string name="prefs_other_switch_update">Автоматичне оновлення даних</string>
|
||||||
|
<string name="prefs_other_switch_lotw_sync">Автосинхронізація підтверджених сіток LoTW</string>
|
||||||
<string name="prefs_other_switch_sweep">Показувати анімацію радара</string>
|
<string name="prefs_other_switch_sweep">Показувати анімацію радара</string>
|
||||||
<string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string>
|
<string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string>
|
||||||
<string name="prefs_other_switch_night_mode">Увімкнути червоний нічний фільтр</string>
|
<string name="prefs_other_switch_night_mode">Увімкнути червоний нічний фільтр</string>
|
||||||
|
|||||||
@@ -237,6 +237,7 @@
|
|||||||
<string name="prefs_other_title">其他设置</string>
|
<string name="prefs_other_title">其他设置</string>
|
||||||
<string name="prefs_other_switch_utc">以 UTC 时间显示</string>
|
<string name="prefs_other_switch_utc">以 UTC 时间显示</string>
|
||||||
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
|
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
|
||||||
|
<string name="prefs_other_switch_lotw_sync">自动同步 LoTW 已确认网格</string>
|
||||||
<string name="prefs_other_switch_sweep">启用雷达扫描动画</string>
|
<string name="prefs_other_switch_sweep">启用雷达扫描动画</string>
|
||||||
<string name="prefs_other_switch_sensors">使用传感器旋转雷达视图</string>
|
<string name="prefs_other_switch_sensors">使用传感器旋转雷达视图</string>
|
||||||
<string name="prefs_other_switch_night_mode">启用红色夜间模式</string>
|
<string name="prefs_other_switch_night_mode">启用红色夜间模式</string>
|
||||||
|
|||||||
@@ -271,6 +271,7 @@
|
|||||||
<string name="prefs_other_title">Other settings</string>
|
<string name="prefs_other_title">Other settings</string>
|
||||||
<string name="prefs_other_switch_utc">Show pass time in UTC</string>
|
<string name="prefs_other_switch_utc">Show pass time in UTC</string>
|
||||||
<string name="prefs_other_switch_update">Enable automatic data update</string>
|
<string name="prefs_other_switch_update">Enable automatic data update</string>
|
||||||
|
<string name="prefs_other_switch_lotw_sync">Auto-sync LoTW confirmed grids</string>
|
||||||
<string name="prefs_other_switch_sweep">Enable radar sweep animation</string>
|
<string name="prefs_other_switch_sweep">Enable radar sweep animation</string>
|
||||||
<string name="prefs_other_switch_sensors">Use sensors to rotate radar view</string>
|
<string name="prefs_other_switch_sensors">Use sensors to rotate radar view</string>
|
||||||
<string name="prefs_other_switch_night_mode">Enable red night mode filter</string>
|
<string name="prefs_other_switch_night_mode">Enable red night mode filter</string>
|
||||||
|
|||||||
+1
@@ -87,6 +87,7 @@ import androidx.compose.ui.zIndex
|
|||||||
import com.rtbishop.look4sat.core.domain.model.Constants
|
import com.rtbishop.look4sat.core.domain.model.Constants
|
||||||
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
import com.rtbishop.look4sat.core.domain.model.RCSettings
|
||||||
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode
|
||||||
import com.rtbishop.look4sat.core.domain.source.NetworkResult
|
import com.rtbishop.look4sat.core.domain.source.NetworkResult
|
||||||
import com.rtbishop.look4sat.core.domain.source.Sources
|
import com.rtbishop.look4sat.core.domain.source.Sources
|
||||||
import com.rtbishop.look4sat.core.presentation.CardButton
|
import com.rtbishop.look4sat.core.presentation.CardButton
|
||||||
|
|||||||
+5
-1
@@ -67,6 +67,7 @@ import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
|||||||
import com.rtbishop.look4sat.core.domain.model.WavelogSettings
|
import com.rtbishop.look4sat.core.domain.model.WavelogSettings
|
||||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode
|
||||||
import com.rtbishop.look4sat.core.presentation.CardButton
|
import com.rtbishop.look4sat.core.presentation.CardButton
|
||||||
import com.rtbishop.look4sat.core.presentation.IconCard
|
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||||
@@ -536,7 +537,7 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
|||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(272.dp)
|
.height(320.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||||
Text(
|
Text(
|
||||||
@@ -549,6 +550,9 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
|||||||
SwitchRow(R.string.prefs_other_switch_update, settings.stateOfAutoUpdate) {
|
SwitchRow(R.string.prefs_other_switch_update, settings.stateOfAutoUpdate) {
|
||||||
onAction(SettingsAction.ToggleUpdate(it))
|
onAction(SettingsAction.ToggleUpdate(it))
|
||||||
}
|
}
|
||||||
|
SwitchRow(R.string.prefs_other_switch_lotw_sync, settings.stateOfAutoLotwSync) {
|
||||||
|
onAction(SettingsAction.ToggleAutoLotwSync(it))
|
||||||
|
}
|
||||||
SwitchRow(R.string.prefs_other_switch_sweep, settings.stateOfSweep) {
|
SwitchRow(R.string.prefs_other_switch_sweep, settings.stateOfSweep) {
|
||||||
onAction(SettingsAction.ToggleSweep(it))
|
onAction(SettingsAction.ToggleSweep(it))
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -24,6 +24,7 @@ import com.rtbishop.look4sat.core.domain.model.RCSettings
|
|||||||
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||||
import com.rtbishop.look4sat.core.domain.model.WavelogSettings
|
import com.rtbishop.look4sat.core.domain.model.WavelogSettings
|
||||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
data class PositionSettings(
|
data class PositionSettings(
|
||||||
@@ -67,9 +68,6 @@ data class SettingsState(
|
|||||||
val updateChecker: UpdateCheckerState = UpdateCheckerState()
|
val updateChecker: UpdateCheckerState = UpdateCheckerState()
|
||||||
)
|
)
|
||||||
|
|
||||||
/** What a LoTW sync button does: pull everything, or only new QSLs since the last sync. */
|
|
||||||
enum class LoTWSyncMode { Full, Incremental }
|
|
||||||
|
|
||||||
/** LoTW sync failure, kept as a translatable code until the UI renders it. */
|
/** LoTW sync failure, kept as a translatable code until the UI renders it. */
|
||||||
sealed interface LoTWError {
|
sealed interface LoTWError {
|
||||||
data object NotConfigured : LoTWError
|
data object NotConfigured : LoTWError
|
||||||
@@ -95,6 +93,7 @@ sealed interface SettingsAction {
|
|||||||
// Toggles
|
// Toggles
|
||||||
data class ToggleUtc(val value: Boolean) : SettingsAction
|
data class ToggleUtc(val value: Boolean) : SettingsAction
|
||||||
data class ToggleUpdate(val value: Boolean) : SettingsAction
|
data class ToggleUpdate(val value: Boolean) : SettingsAction
|
||||||
|
data class ToggleAutoLotwSync(val value: Boolean) : SettingsAction
|
||||||
data class ToggleSweep(val value: Boolean) : SettingsAction
|
data class ToggleSweep(val value: Boolean) : SettingsAction
|
||||||
data class ToggleSensor(val value: Boolean) : SettingsAction
|
data class ToggleSensor(val value: Boolean) : SettingsAction
|
||||||
data class ToggleLightTheme(val value: Boolean) : SettingsAction
|
data class ToggleLightTheme(val value: Boolean) : SettingsAction
|
||||||
|
|||||||
+14
-56
@@ -27,6 +27,10 @@ import com.rtbishop.look4sat.core.domain.model.WavelogSettings
|
|||||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IUpdateRepository
|
import com.rtbishop.look4sat.core.domain.repository.IUpdateRepository
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.LoTWResult
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.applyLoTWGridResult
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.resolveLoTWSyncMode
|
||||||
import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository
|
import com.rtbishop.look4sat.core.domain.repository.IWavelogRepository
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||||
import com.rtbishop.look4sat.core.domain.utility.VersionComparator
|
import com.rtbishop.look4sat.core.domain.utility.VersionComparator
|
||||||
@@ -35,7 +39,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
@@ -148,6 +151,7 @@ class SettingsViewModel(
|
|||||||
// Toggles
|
// Toggles
|
||||||
is SettingsAction.ToggleUtc -> settingsRepo.updateOtherSettings { it.copy(stateOfUtc = action.value) }
|
is SettingsAction.ToggleUtc -> settingsRepo.updateOtherSettings { it.copy(stateOfUtc = action.value) }
|
||||||
is SettingsAction.ToggleUpdate -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoUpdate = action.value) }
|
is SettingsAction.ToggleUpdate -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoUpdate = action.value) }
|
||||||
|
is SettingsAction.ToggleAutoLotwSync -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoLotwSync = action.value) }
|
||||||
is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) }
|
is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) }
|
||||||
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
|
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
|
||||||
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
|
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
|
||||||
@@ -209,10 +213,7 @@ class SettingsViewModel(
|
|||||||
// a first sync, an unknown/empty record or a callsign change must fall
|
// a first sync, an unknown/empty record or a callsign change must fall
|
||||||
// back to a full report so no stale grids from another account linger.
|
// back to a full report so no stale grids from another account linger.
|
||||||
val callsign = settings.callsign.trim().uppercase()
|
val callsign = settings.callsign.trim().uppercase()
|
||||||
val lastCallsign = settingsRepo.getLastLotwSyncCallsign()
|
val effectiveMode = resolveLoTWSyncMode(settingsRepo.getLastLotwSyncCallsign(), callsign, mode)
|
||||||
val effectiveMode = if (mode == LoTWSyncMode.Incremental &&
|
|
||||||
lastCallsign.isNotBlank() && lastCallsign == callsign
|
|
||||||
) LoTWSyncMode.Incremental else LoTWSyncMode.Full
|
|
||||||
val since = if (effectiveMode == LoTWSyncMode.Incremental) {
|
val since = if (effectiveMode == LoTWSyncMode.Incremental) {
|
||||||
settingsRepo.getLastLotwSyncDate()
|
settingsRepo.getLastLotwSyncDate()
|
||||||
} else ""
|
} else ""
|
||||||
@@ -224,67 +225,40 @@ class SettingsViewModel(
|
|||||||
_uiState.update { it.copy(lotwProgress = progress) }
|
_uiState.update { it.copy(lotwProgress = progress) }
|
||||||
}
|
}
|
||||||
when (result) {
|
when (result) {
|
||||||
is com.rtbishop.look4sat.core.domain.repository.LoTWResult.Success -> {
|
is LoTWResult.Success -> {
|
||||||
// Incremental: merge new grids/QSOs into the stored set (dedup
|
// Incremental: merge new grids/QSOs into the stored set (dedup
|
||||||
// by call + QSO time); full: the fresh report replaces it all.
|
// by call + QSO time); full: the fresh report replaces it all.
|
||||||
val existingGrids = settingsRepo.getWorkedGrids()
|
// Shared with the automatic sync on app start (MainApplication).
|
||||||
val existingQsos = settingsRepo.getWorkedGridQsos()
|
val mergedGridsCount = applyLoTWGridResult(settingsRepo, result, effectiveMode, callsign)
|
||||||
val existingRoamed = settingsRepo.getRoamedGrids()
|
|
||||||
val mergedGrids = if (effectiveMode == LoTWSyncMode.Incremental) {
|
|
||||||
existingGrids + result.grids
|
|
||||||
} else result.grids
|
|
||||||
val mergedQsos = if (effectiveMode == LoTWSyncMode.Incremental) {
|
|
||||||
mergeGridQsos(existingQsos, result.qsos)
|
|
||||||
} else result.qsos
|
|
||||||
val mergedRoamed = if (effectiveMode == LoTWSyncMode.Incremental) {
|
|
||||||
existingRoamed + result.roamedGrids
|
|
||||||
} else result.roamedGrids
|
|
||||||
// Remember when/what we synced, so the next incremental pull
|
|
||||||
// asks LoTW for only the confirmations since today.
|
|
||||||
val today = java.text.SimpleDateFormat("yyyyMMdd", java.util.Locale.US)
|
|
||||||
.apply { timeZone = java.util.TimeZone.getTimeZone("UTC") }
|
|
||||||
.format(java.util.Date())
|
|
||||||
settingsRepo.setLastLotwSyncDate(today)
|
|
||||||
settingsRepo.setLastLotwSyncCallsign(callsign)
|
|
||||||
// Persist on the IO dispatcher: building the per-grid QSO JSON
|
|
||||||
// and writing SharedPreferences synchronously blocks the
|
|
||||||
// calling thread, and for large accounts (tens of thousands
|
|
||||||
// of QSOs → multi-MB JSON) that froze the main thread after
|
|
||||||
// the download finished — 'bar done but app stuck'.
|
|
||||||
withContext(kotlinx.coroutines.Dispatchers.IO) {
|
|
||||||
settingsRepo.setWorkedGrids(mergedGrids)
|
|
||||||
settingsRepo.setWorkedGridQsos(mergedQsos)
|
|
||||||
settingsRepo.setRoamedGrids(mergedRoamed)
|
|
||||||
}
|
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||||
workedGridsCount = mergedGrids.size, lotwError = null
|
workedGridsCount = mergedGridsCount, lotwError = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is com.rtbishop.look4sat.core.domain.repository.LoTWResult.BadCredentials ->
|
is LoTWResult.BadCredentials ->
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||||
lotwError = LoTWError.BadCredentials
|
lotwError = LoTWError.BadCredentials
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is com.rtbishop.look4sat.core.domain.repository.LoTWResult.RateLimited ->
|
is LoTWResult.RateLimited ->
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||||
lotwError = LoTWError.RateLimited
|
lotwError = LoTWError.RateLimited
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is com.rtbishop.look4sat.core.domain.repository.LoTWResult.Timeout ->
|
is LoTWResult.Timeout ->
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||||
lotwError = LoTWError.Timeout
|
lotwError = LoTWError.Timeout
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is com.rtbishop.look4sat.core.domain.repository.LoTWResult.NetworkError ->
|
is LoTWResult.NetworkError ->
|
||||||
_uiState.update { state ->
|
_uiState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||||
@@ -302,22 +276,6 @@ class SettingsViewModel(
|
|||||||
_uiState.update { it.copy(lotwSyncing = false, lotwSyncMode = null, lotwProgress = null) }
|
_uiState.update { it.copy(lotwSyncing = false, lotwSyncMode = null, lotwProgress = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Merge fresh QSO detail into existing per-grid lists, dedup by call + QSO time. */
|
|
||||||
private fun mergeGridQsos(
|
|
||||||
existing: Map<String, List<com.rtbishop.look4sat.core.domain.model.GridQso>>,
|
|
||||||
fresh: Map<String, List<com.rtbishop.look4sat.core.domain.model.GridQso>>
|
|
||||||
): Map<String, List<com.rtbishop.look4sat.core.domain.model.GridQso>> {
|
|
||||||
val merged = existing.mapValues { (_, list) -> list.toMutableList() }.toMutableMap()
|
|
||||||
fresh.forEach { (grid, list) ->
|
|
||||||
val target = merged.getOrPut(grid) { mutableListOf() }
|
|
||||||
val known = target.mapTo(mutableSetOf()) { it.call to it.epochMs }
|
|
||||||
list.forEach { qso ->
|
|
||||||
if (known.add(qso.call to qso.epochMs)) target.add(qso)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return merged
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Update checker
|
// region Update checker
|
||||||
|
|||||||
Reference in new issue
Block a user