mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat(lotw): show last sync time on settings, precise incremental cursor
The stored sync cursor was 'yyyyMMdd', which LoTW's qso_qslsince silently ignores (measured live: it falls back to the system-supplied default). Write a full UTC 'yyyy-MM-dd HH:mm:ss' cursor instead — the documented format LoTW honors — so incremental pulls are exact (second precision) and the settings page can show the last successful sync time on the LoTW card, formatted like the ephemeris update time (prefs_updated_title). Legacy yyyyMMdd cursors are normalized for the API, the gate and the display (lotwCursorApi/lotwCursorDate/lotwCursorEpochMs).
This commit is contained in:
1 parent
60dfa5d600
commit
059c816075
6 files changed
+105
-15
No files matched your search
+47
-3
@@ -36,12 +36,53 @@ import java.util.TimeZone
|
||||
* - [LoTWSyncMode.Full] pulls everything and REPLACES the stored data.
|
||||
*/
|
||||
|
||||
/** UTC date ("yyyyMMdd") of the given instant — the LoTW sync cursor granularity. */
|
||||
/**
|
||||
* UTC date ("yyyyMMdd") of the given instant — the "already synced today"
|
||||
* gate granularity.
|
||||
*/
|
||||
fun lotwSyncToday(now: Long = System.currentTimeMillis()): String =
|
||||
SimpleDateFormat("yyyyMMdd", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
.format(Date(now))
|
||||
|
||||
/**
|
||||
* Full UTC timestamp cursor ("yyyy-MM-dd HH:mm:ss") written after a
|
||||
* successful sync. This is exactly the format LoTW's qso_qslsince accepts
|
||||
* (measured live 2026-09-17: the legacy "yyyyMMdd" cursor is silently
|
||||
* ignored by LoTW, which falls back to its own system-supplied default),
|
||||
* and it keeps enough precision to display the last-sync time like the
|
||||
* ephemeris update time.
|
||||
*/
|
||||
fun lotwSyncCursor(now: Long = System.currentTimeMillis()): String =
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
.format(Date(now))
|
||||
|
||||
/** Date part of a stored cursor as "yyyyMMdd", tolerating the legacy
|
||||
* "yyyyMMdd" format and the current "yyyy-MM-dd HH:mm:ss" format. */
|
||||
fun lotwCursorDate(cursor: String): String = when {
|
||||
cursor.length >= 10 && cursor[4] == '-' -> cursor.substring(0, 10).replace("-", "")
|
||||
else -> cursor.take(8)
|
||||
}
|
||||
|
||||
/** qso_qslsince value for the API: passes the full timestamp through and
|
||||
* normalizes a legacy "yyyyMMdd" cursor to the documented "yyyy-MM-dd". */
|
||||
fun lotwCursorApi(cursor: String): String = when {
|
||||
cursor.isBlank() -> ""
|
||||
cursor.length >= 10 && cursor[4] == '-' -> cursor
|
||||
else -> "${cursor.take(4)}-${cursor.substring(4, 6)}-${cursor.substring(6, 8)}"
|
||||
}
|
||||
|
||||
/** Stored cursor parsed to UTC epoch ms for display; 0 when unparseable. */
|
||||
fun lotwCursorEpochMs(cursor: String): Long = try {
|
||||
val pattern = if (cursor.length >= 10 && cursor[4] == '-') "yyyy-MM-dd HH:mm:ss" else "yyyyMMdd"
|
||||
SimpleDateFormat(pattern, Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
.parse(cursor)?.time ?: 0L
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the effective sync mode for a given request:
|
||||
* - an explicit [LoTWSyncMode.Full] request is always honored;
|
||||
@@ -73,7 +114,10 @@ fun shouldAutoSyncLoTW(
|
||||
autoLotwSyncEnabled: Boolean,
|
||||
lastSyncDate: String,
|
||||
today: String
|
||||
): Boolean = isConfigured && autoLotwSyncEnabled && lastSyncDate.isNotBlank() && lastSyncDate != today
|
||||
): Boolean {
|
||||
val lastDate = lotwCursorDate(lastSyncDate)
|
||||
return isConfigured && autoLotwSyncEnabled && lastDate.isNotBlank() && lastDate != today
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a successful LoTW report to the stored grid data and advances the
|
||||
@@ -98,7 +142,7 @@ suspend fun applyLoTWGridResult(
|
||||
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.setLastLotwSyncDate(lotwSyncCursor(now))
|
||||
settingsRepo.setLastLotwSyncCallsign(callsign)
|
||||
withContext(Dispatchers.IO) {
|
||||
settingsRepo.setWorkedGrids(mergedGrids)
|
||||
|
||||
+37
-8
@@ -79,15 +79,44 @@ class LoTWGridSyncTest {
|
||||
fun autoSyncGateRequiresEverything() {
|
||||
val today = "20260916"
|
||||
// Not configured.
|
||||
assertFalse(shouldAutoSyncLoTW(false, true, "20260915", today))
|
||||
// Auto-update toggle off.
|
||||
assertFalse(shouldAutoSyncLoTW(true, false, "20260915", today))
|
||||
assertFalse(shouldAutoSyncLoTW(false, true, "2026-09-15 10:00:00", today))
|
||||
// LoTW auto-sync toggle off.
|
||||
assertFalse(shouldAutoSyncLoTW(true, false, "2026-09-15 10:00:00", 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.
|
||||
// Already synced today (both legacy and current cursor formats) — ARRL rate-limit guard.
|
||||
assertFalse(shouldAutoSyncLoTW(true, true, "20260916", today))
|
||||
assertFalse(shouldAutoSyncLoTW(true, true, "2026-09-16 08:00:00", today))
|
||||
// All gates open — yesterday in either format.
|
||||
assertTrue(shouldAutoSyncLoTW(true, true, "20260915", today))
|
||||
assertTrue(shouldAutoSyncLoTW(true, true, "2026-09-15 10:00:00", today))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region cursor helpers
|
||||
|
||||
@Test
|
||||
fun cursorApiNormalizesLegacyAndPassesFullThrough() {
|
||||
assertEquals("2026-09-16", lotwCursorApi("20260916"))
|
||||
assertEquals("2026-09-16 08:16:02", lotwCursorApi("2026-09-16 08:16:02"))
|
||||
assertEquals("", lotwCursorApi(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cursorDateToleratesBothFormats() {
|
||||
assertEquals("20260916", lotwCursorDate("20260916"))
|
||||
assertEquals("20260916", lotwCursorDate("2026-09-16 08:16:02"))
|
||||
assertEquals("", lotwCursorDate(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cursorEpochMsParsesBothFormatsUtc() {
|
||||
// 2026-09-16T00:00:00Z in both spellings.
|
||||
assertEquals(now, lotwCursorEpochMs("20260916"))
|
||||
assertEquals(now, lotwCursorEpochMs("2026-09-16 00:00:00"))
|
||||
assertEquals(0L, lotwCursorEpochMs(""))
|
||||
assertEquals(0L, lotwCursorEpochMs("garbage"))
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -148,7 +177,7 @@ class LoTWGridSyncTest {
|
||||
assertEquals(setOf("OL62", "PM95"), repo.getWorkedGrids())
|
||||
assertEquals(1, repo.getWorkedGridQsos()["OL62"]?.size) // deduped
|
||||
assertEquals(setOf("OL62", "PM95"), repo.getRoamedGrids())
|
||||
assertEquals("20260916", repo.getLastLotwSyncDate())
|
||||
assertEquals("2026-09-16 00:00:00", repo.getLastLotwSyncDate())
|
||||
assertEquals("BA7OPF", repo.getLastLotwSyncCallsign())
|
||||
}
|
||||
|
||||
@@ -172,7 +201,7 @@ class LoTWGridSyncTest {
|
||||
assertEquals(setOf("PM95"), repo.getWorkedGrids())
|
||||
assertEquals(setOf("PM95"), repo.getWorkedGridQsos().keys)
|
||||
assertEquals(setOf("PM95"), repo.getRoamedGrids())
|
||||
assertEquals("20260916", repo.getLastLotwSyncDate())
|
||||
assertEquals("2026-09-16 00:00:00", repo.getLastLotwSyncDate())
|
||||
assertEquals("BA7OPF", repo.getLastLotwSyncCallsign())
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user