mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +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
+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.RCSettings
|
||||
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.Sources
|
||||
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.predict.GeoPos
|
||||
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.IconCard
|
||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||
@@ -536,7 +537,7 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(272.dp)
|
||||
.height(320.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
Text(
|
||||
@@ -549,6 +550,9 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
||||
SwitchRow(R.string.prefs_other_switch_update, settings.stateOfAutoUpdate) {
|
||||
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) {
|
||||
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.WavelogSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode
|
||||
import java.io.File
|
||||
|
||||
data class PositionSettings(
|
||||
@@ -67,9 +68,6 @@ data class SettingsState(
|
||||
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. */
|
||||
sealed interface LoTWError {
|
||||
data object NotConfigured : LoTWError
|
||||
@@ -95,6 +93,7 @@ sealed interface SettingsAction {
|
||||
// Toggles
|
||||
data class ToggleUtc(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 ToggleSensor(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.ISettingsRepo
|
||||
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.usecase.IShowToast
|
||||
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.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
class SettingsViewModel(
|
||||
@@ -148,6 +151,7 @@ class SettingsViewModel(
|
||||
// Toggles
|
||||
is SettingsAction.ToggleUtc -> settingsRepo.updateOtherSettings { it.copy(stateOfUtc = 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.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = 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
|
||||
// back to a full report so no stale grids from another account linger.
|
||||
val callsign = settings.callsign.trim().uppercase()
|
||||
val lastCallsign = settingsRepo.getLastLotwSyncCallsign()
|
||||
val effectiveMode = if (mode == LoTWSyncMode.Incremental &&
|
||||
lastCallsign.isNotBlank() && lastCallsign == callsign
|
||||
) LoTWSyncMode.Incremental else LoTWSyncMode.Full
|
||||
val effectiveMode = resolveLoTWSyncMode(settingsRepo.getLastLotwSyncCallsign(), callsign, mode)
|
||||
val since = if (effectiveMode == LoTWSyncMode.Incremental) {
|
||||
settingsRepo.getLastLotwSyncDate()
|
||||
} else ""
|
||||
@@ -224,67 +225,40 @@ class SettingsViewModel(
|
||||
_uiState.update { it.copy(lotwProgress = progress) }
|
||||
}
|
||||
when (result) {
|
||||
is com.rtbishop.look4sat.core.domain.repository.LoTWResult.Success -> {
|
||||
is LoTWResult.Success -> {
|
||||
// Incremental: merge new grids/QSOs into the stored set (dedup
|
||||
// by call + QSO time); full: the fresh report replaces it all.
|
||||
val existingGrids = settingsRepo.getWorkedGrids()
|
||||
val existingQsos = settingsRepo.getWorkedGridQsos()
|
||||
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)
|
||||
}
|
||||
// Shared with the automatic sync on app start (MainApplication).
|
||||
val mergedGridsCount = applyLoTWGridResult(settingsRepo, result, effectiveMode, callsign)
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
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 ->
|
||||
state.copy(
|
||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||
lotwError = LoTWError.BadCredentials
|
||||
)
|
||||
}
|
||||
is com.rtbishop.look4sat.core.domain.repository.LoTWResult.RateLimited ->
|
||||
is LoTWResult.RateLimited ->
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||
lotwError = LoTWError.RateLimited
|
||||
)
|
||||
}
|
||||
is com.rtbishop.look4sat.core.domain.repository.LoTWResult.Timeout ->
|
||||
is LoTWResult.Timeout ->
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||
lotwError = LoTWError.Timeout
|
||||
)
|
||||
}
|
||||
is com.rtbishop.look4sat.core.domain.repository.LoTWResult.NetworkError ->
|
||||
is LoTWResult.NetworkError ->
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
lotwSyncing = false, lotwSyncMode = null, lotwProgress = null,
|
||||
@@ -302,22 +276,6 @@ class SettingsViewModel(
|
||||
_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
|
||||
|
||||
// region Update checker
|
||||
|
||||
Reference in new issue
Block a user