mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-09 13:57:24 +00:00
feat(wavelog): 二选一互斥模式 + p12 空密码导入修复(4.4.8-ba7opf.9.4, 569)
- LoTW/Wavelog 互斥:isReady 即 Wavelog 模式,LoTW 卡置灰+说明,上传按模式分流(雷达页/日志本),自动 LoTW 同步门控;配置切换弹警告、可清除配置 - 同步重做:get_contacts_adif 每台址游标,增量/全量双按钮,只增不删,URL 变更增量降级全量;同步刷新日志本+地图 workedGrids/gridQsos/roamedGrids - 日志本台址选择器(全部/各台址)+ Wavelog 上传预览弹窗;Qso 增 wavelogStation,DB v4→v5 - 删旧 WavelogRepository/wavelogSettings 链路,换 WavelogSyncRepository + WavelogSync 纯函数 - p12 修复:导入按钮放开空密码(isEnabled=!busy)+ 密码框留空提示;Pkcs12ReaderTest +4 用例 - 单测六模块全绿;版本 4.4.8-ba7opf.9.4 (569)
This commit is contained in:
1 parent
1fa7dd2d88
commit
09539ff13a
40 files changed
+1715
-470
No files matched your search
+14
-2
@@ -24,14 +24,26 @@ interface IQsoRepository {
|
||||
*/
|
||||
suspend fun markUploaded(ids: List<Long>, grids: List<String> = emptyList())
|
||||
|
||||
/** Marks the batch as uploaded to Wavelog (independent of the LoTW upload state). */
|
||||
suspend fun markWavelogUploaded(ids: List<Long>)
|
||||
/**
|
||||
* Marks the batch as uploaded to Wavelog (independent of the LoTW upload state) and
|
||||
* stamps the station profile (台址) the batch went out through, so the logbook's
|
||||
* 台址 selector can group the records. An empty [stationId] leaves the stamp as-is.
|
||||
*/
|
||||
suspend fun markWavelogUploaded(ids: List<Long>, stationId: String = "")
|
||||
|
||||
suspend fun exportAdi(ids: Set<Long>? = null, includeIncomplete: Boolean = false): String
|
||||
suspend fun importAdi(content: String): AdifImportResult
|
||||
suspend fun mergeConfirmed(records: List<QsoRecord>): AdifImportResult
|
||||
suspend fun mergeLoTW(records: List<QsoRecord>): AdifImportResult
|
||||
|
||||
/**
|
||||
* Merges QSO records pulled from Wavelog by a sync (the download direction):
|
||||
* matches by the same stable identity as every other import, tags matched and
|
||||
* freshly inserted rows with their Wavelog station profile, and marks them as
|
||||
* already uploaded so a later upload never sends them straight back.
|
||||
*/
|
||||
suspend fun mergeWavelog(records: List<QsoRecord>): AdifImportResult
|
||||
|
||||
/**
|
||||
* Folds LoTW confirmations that were imported as their own rows back into the local
|
||||
* record they belong to (see [com.rtbishop.look4sat.core.domain.logbook.splitConfirmationPairs]).
|
||||
|
||||
@@ -42,6 +42,10 @@ data class QsoRecord(
|
||||
val lotwUploaded: Boolean = false,
|
||||
/** Internal: set after a successful Wavelog upload; keeps the pending filter small. Not shown in any list. */
|
||||
val wavelogUploaded: Boolean = false,
|
||||
/** Wavelog station profile (台址) this QSO belongs to: the profile it was uploaded
|
||||
* through, or the profile a sync pulled it from. Empty for records the app never
|
||||
* involved with Wavelog — the logbook's 台址 selector shows those under "All" only. */
|
||||
val wavelogStation: String = "",
|
||||
val lotwReceived: Boolean = false,
|
||||
val lotwQslDate: String = "",
|
||||
val vuccGrids: List<String> = emptyList(),
|
||||
|
||||
@@ -102,14 +102,7 @@ data class DataSourcesSettings(
|
||||
fun isTransceiverEnabled(index: Int): Boolean = transceiversEnabled.getOrElse(index) { true }
|
||||
}
|
||||
|
||||
data class WavelogSettings(
|
||||
val url: String = "",
|
||||
val token: String = ""
|
||||
) {
|
||||
val isConfigured: Boolean get() = url.isNotBlank() && token.isNotBlank()
|
||||
}
|
||||
|
||||
/** Wavelog QSO-upload configuration (a separate block from the worked-grids sync). */
|
||||
/** Wavelog configuration — QSO upload both ways; one of LoTW or Wavelog (mutually exclusive). */
|
||||
data class WavelogUploadSettings(
|
||||
val url: String = "",
|
||||
val apiKey: String = "",
|
||||
|
||||
+1
-1
@@ -33,8 +33,8 @@ interface IMainContainer {
|
||||
val databaseRepo: IDatabaseRepo
|
||||
val amSatRepo: IAmSatRepository
|
||||
val updateRepo: IUpdateRepository
|
||||
val wavelogRepo: IWavelogRepository
|
||||
val wavelogUploadRepository: IWavelogUploadRepository
|
||||
val wavelogSyncRepository: IWavelogSyncRepository
|
||||
val lotwRepo: com.rtbishop.look4sat.core.domain.repository.ILoTWRepository
|
||||
val qsoRepository: IQsoRepository
|
||||
val lotwUploadRepository: ILoTWUploadRepository
|
||||
|
||||
+17
-4
@@ -23,7 +23,7 @@ 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.model.WavelogUploadSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -107,9 +107,7 @@ interface ISettingsRepo {
|
||||
fun setAmSatCallsign(callsign: String)
|
||||
//endregion
|
||||
|
||||
//region # Wavelog worked-grids settings
|
||||
val wavelogSettings: StateFlow<WavelogSettings>
|
||||
fun updateWavelogSettings(settings: WavelogSettings)
|
||||
//region # Worked-grid data (written by the LoTW / Wavelog syncs)
|
||||
fun getWorkedGrids(): Set<String>
|
||||
fun setWorkedGrids(grids: Set<String>)
|
||||
/** Confirmed satellite QSOs grouped by worked 4-char gridsquare (map tap detail). */
|
||||
@@ -130,6 +128,21 @@ interface ISettingsRepo {
|
||||
fun updateWavelogUploadSettings(settings: WavelogUploadSettings)
|
||||
//endregion
|
||||
|
||||
//region # Wavelog sync bookkeeping
|
||||
/** Station profiles as last reported by the server — labels for the 台址 selector and targets for sync. */
|
||||
fun getWavelogStations(): List<WavelogStationInfo>
|
||||
fun setWavelogStations(stations: List<WavelogStationInfo>)
|
||||
/** stationId -> lastfetchedid cursors of the last successful sync (per station profile). */
|
||||
fun getWavelogSyncCursors(): Map<String, Long>
|
||||
fun setWavelogSyncCursors(cursors: Map<String, Long>)
|
||||
/** Wavelog base URL the stored cursors belong to (empty when never synced). */
|
||||
fun getWavelogSyncUrl(): String
|
||||
fun setWavelogSyncUrl(url: String)
|
||||
/** Epoch ms of the last successful Wavelog sync (0 when never synced). */
|
||||
fun getLastWavelogSyncEpochMs(): Long
|
||||
fun setLastWavelogSyncEpochMs(value: Long)
|
||||
//endregion
|
||||
|
||||
//region # LoTW confirmed-grids settings
|
||||
val lotwSettings: StateFlow<com.rtbishop.look4sat.core.domain.model.LoTWSettings>
|
||||
fun updateLoTWSettings(settings: com.rtbishop.look4sat.core.domain.model.LoTWSettings)
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
/** Fetches worked gridsquares from a self-hosted Wavelog instance (API v2). */
|
||||
interface IWavelogRepository {
|
||||
|
||||
/**
|
||||
* Fetch worked grids for the configured Wavelog URL/token.
|
||||
* Returns the grid set, or null on any failure.
|
||||
*/
|
||||
suspend fun fetchWorkedGrids(url: String, token: String): Set<String>?
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Look4Sat-BA7OPF. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2026 BA7OPF.
|
||||
* Based on Look4Sat by 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.logbook.QsoRecord
|
||||
|
||||
/** What a Wavelog sync pulls: everything, or only records newer than the stored cursors. */
|
||||
enum class WavelogSyncMode { Full, Incremental }
|
||||
|
||||
/** Outcome of one sync fetch. */
|
||||
data class WavelogSyncFetch(
|
||||
/** Records pulled this run, each tagged with the station profile it came from. */
|
||||
val records: List<QsoRecord>,
|
||||
/** stationId -> lastfetchedid to resume from on the next incremental run. */
|
||||
val cursors: Map<String, Long>,
|
||||
/** Station ids that could not be fetched (transport/parse); empty when all succeeded. */
|
||||
val failedStations: List<String>
|
||||
)
|
||||
|
||||
/**
|
||||
* Pulls QSO records from a self-hosted Wavelog (v1 API, `api/get_contacts_adif`).
|
||||
* One request per station profile; the server exports only rows newer than the
|
||||
* `fetchfromid` cursor, so repeated syncs stay incremental per station.
|
||||
*/
|
||||
interface IWavelogSyncRepository {
|
||||
|
||||
/**
|
||||
* Fetches records for every entry of [stations]. [cursors] maps stationId to the
|
||||
* last id already synced (missing -> 0); [full] re-pulls each station from the
|
||||
* beginning, ignoring the cursors. Returns null when the fetch cannot run at all
|
||||
* (blank url/key or no stations); individual station failures land in
|
||||
* [WavelogSyncFetch.failedStations] with the remaining stations still fetched.
|
||||
*/
|
||||
suspend fun fetchNewRecords(
|
||||
url: String,
|
||||
apiKey: String,
|
||||
stations: List<WavelogStationInfo>,
|
||||
cursors: Map<String, Long>,
|
||||
full: Boolean
|
||||
): WavelogSyncFetch?
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Look4Sat-BA7OPF. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2026 BA7OPF.
|
||||
* Based on Look4Sat by 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.logbook.QsoRecord
|
||||
import com.rtbishop.look4sat.core.domain.model.GridQso
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
|
||||
/** Trimmed base form of a Wavelog server URL (".../wavelog/" -> ".../wavelog"). */
|
||||
fun normalizeWavelogUrl(url: String): String = url.trim().trimEnd('/')
|
||||
|
||||
/**
|
||||
* Resolves the effective sync mode for a request:
|
||||
* - an explicit [WavelogSyncMode.Full] is always honored;
|
||||
* - an [WavelogSyncMode.Incremental] request is honored only when the stored sync URL
|
||||
* matches the current one — a first sync or a server switch falls back to
|
||||
* [WavelogSyncMode.Full], so cursors from another server never leak in.
|
||||
*/
|
||||
fun resolveWavelogSyncMode(storedUrl: String, currentUrl: String, requested: WavelogSyncMode?): WavelogSyncMode {
|
||||
if (requested == WavelogSyncMode.Full) return WavelogSyncMode.Full
|
||||
val sameServer = storedUrl.isNotBlank() && storedUrl == normalizeWavelogUrl(currentUrl)
|
||||
return if (sameServer) WavelogSyncMode.Incremental else WavelogSyncMode.Full
|
||||
}
|
||||
|
||||
/** Grid data derived from Wavelog-synced records, in the same shape the LoTW sync produces. */
|
||||
data class WavelogGridData(
|
||||
val grids: Set<String>,
|
||||
val gridQsos: Map<String, List<GridQso>>,
|
||||
val roamedGrids: Set<String>
|
||||
)
|
||||
|
||||
internal fun isValidGrid4(grid: String): Boolean =
|
||||
grid.length == 4 && grid[0] in 'A'..'R' && grid[1] in 'A'..'R' &&
|
||||
grid[2] in '0'..'9' && grid[3] in '0'..'9'
|
||||
|
||||
/**
|
||||
* Derives the worked-grid set, the per-grid QSO detail and the roamed (own) grids from
|
||||
* records pulled by a Wavelog sync — mirroring what the LoTW report parser produces, so
|
||||
* both sync sources feed the map's highlight and its 台址 selector the same data shape.
|
||||
*/
|
||||
fun buildWavelogGridData(records: List<QsoRecord>): WavelogGridData {
|
||||
val grids = linkedSetOf<String>()
|
||||
val roamed = linkedSetOf<String>()
|
||||
val gridQsos = linkedMapOf<String, MutableList<GridQso>>()
|
||||
records.forEach { record ->
|
||||
val isSatellite = record.satelliteName.isNotBlank() || record.propagationMode.equals("SAT", true)
|
||||
if (!isSatellite) return@forEach
|
||||
val theirGrids = (record.theirVuccGrids.ifEmpty { listOf(record.theirGrid) })
|
||||
.map { it.trim().uppercase(Locale.US).take(4) }
|
||||
.filter(::isValidGrid4)
|
||||
.distinct()
|
||||
if (theirGrids.isEmpty()) return@forEach
|
||||
val myGrids = (record.vuccGrids.ifEmpty { listOf(record.myGrid) })
|
||||
.map { it.trim().uppercase(Locale.US).take(4) }
|
||||
.filter(::isValidGrid4)
|
||||
.toSet()
|
||||
roamed += myGrids
|
||||
val qso = GridQso(
|
||||
call = record.theirCallsign,
|
||||
epochMs = record.startUtcMillis,
|
||||
satName = record.satelliteName,
|
||||
mode = record.mode,
|
||||
bandUp = record.band,
|
||||
bandDown = record.rxBand,
|
||||
dxcc = record.dxcc,
|
||||
country = record.country.ifBlank { null },
|
||||
cqz = record.cqZone,
|
||||
state = record.region.ifBlank { null },
|
||||
myGrid = myGrids.sorted().firstOrNull(),
|
||||
myGrids = myGrids,
|
||||
myCallsign = record.myCallsign.ifBlank { null },
|
||||
theirGrids = theirGrids
|
||||
)
|
||||
theirGrids.forEach { grid ->
|
||||
grids += grid
|
||||
gridQsos.getOrPut(grid) { mutableListOf() }.add(qso)
|
||||
}
|
||||
}
|
||||
return WavelogGridData(grids, gridQsos, roamed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a successful Wavelog sync to the stored map data and sync bookkeeping.
|
||||
*
|
||||
* Grids, per-grid QSO detail and roamed grids only ever GROW (union): both the
|
||||
* incremental and the full re-pull merge into the stored sets, matching how the LoTW
|
||||
* sync treats stale data — a full re-pull heals missing records but never deletes map
|
||||
* data the app already knows about. Cursors are replaced wholesale ([cursors] covers
|
||||
* every station just fetched), and the sync URL/epoch advance so the next run resumes
|
||||
* incrementally. Persistence runs on the IO dispatcher — building the per-grid QSO
|
||||
* JSON blocks the caller, and for large accounts that froze the main thread after a
|
||||
* manual sync.
|
||||
*
|
||||
* @return the resulting number of worked grids.
|
||||
*/
|
||||
suspend fun applyWavelogSyncResult(
|
||||
settingsRepo: ISettingsRepo,
|
||||
records: List<QsoRecord>,
|
||||
cursors: Map<String, Long>,
|
||||
url: String,
|
||||
now: Long = System.currentTimeMillis()
|
||||
): Int {
|
||||
val data = buildWavelogGridData(records)
|
||||
val existingGrids = settingsRepo.getWorkedGrids()
|
||||
val existingQsos = settingsRepo.getWorkedGridQsos()
|
||||
val existingRoamed = settingsRepo.getRoamedGrids()
|
||||
val mergedGrids = existingGrids + data.grids
|
||||
val mergedQsos = mergeGridQsos(existingQsos, data.gridQsos)
|
||||
val mergedRoamed = existingRoamed + data.roamedGrids
|
||||
settingsRepo.setWavelogSyncCursors(cursors)
|
||||
settingsRepo.setWavelogSyncUrl(normalizeWavelogUrl(url))
|
||||
settingsRepo.setLastWavelogSyncEpochMs(now)
|
||||
withContext(Dispatchers.IO) {
|
||||
settingsRepo.setWorkedGrids(mergedGrids)
|
||||
settingsRepo.setWorkedGridQsos(mergedQsos)
|
||||
settingsRepo.setRoamedGrids(mergedRoamed)
|
||||
}
|
||||
return mergedGrids.size
|
||||
}
|
||||
+16
-3
@@ -26,7 +26,7 @@ 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.model.WavelogUploadSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -248,7 +248,7 @@ class LoTWGridSyncTest {
|
||||
override val radioControlSettings: StateFlow<RadioControlSettings> = MutableStateFlow(
|
||||
RadioControlSettings(false, RadioControlSettings.MODEL_YAESU_FT817, "", "", "", "", 9600)
|
||||
)
|
||||
override val wavelogSettings: StateFlow<WavelogSettings> = MutableStateFlow(WavelogSettings())
|
||||
|
||||
override val wavelogUploadSettings: StateFlow<WavelogUploadSettings> = MutableStateFlow(WavelogUploadSettings())
|
||||
override val lotwSettings: StateFlow<LoTWSettings> = MutableStateFlow(LoTWSettings())
|
||||
|
||||
@@ -272,6 +272,19 @@ class LoTWGridSyncTest {
|
||||
override fun getLastLotwSyncCallsign(): String = lastSyncCallsign
|
||||
override fun setLastLotwSyncCallsign(callsign: String) { lastSyncCallsign = callsign }
|
||||
|
||||
private var wavelogStations: List<com.rtbishop.look4sat.core.domain.repository.WavelogStationInfo> = emptyList()
|
||||
private var wavelogSyncCursors: Map<String, Long> = emptyMap()
|
||||
private var wavelogSyncUrl: String = ""
|
||||
private var lastWavelogSyncEpochMs: Long = 0L
|
||||
override fun getWavelogStations(): List<com.rtbishop.look4sat.core.domain.repository.WavelogStationInfo> = wavelogStations
|
||||
override fun setWavelogStations(stations: List<com.rtbishop.look4sat.core.domain.repository.WavelogStationInfo>) { wavelogStations = stations }
|
||||
override fun getWavelogSyncCursors(): Map<String, Long> = wavelogSyncCursors
|
||||
override fun setWavelogSyncCursors(cursors: Map<String, Long>) { wavelogSyncCursors = cursors }
|
||||
override fun getWavelogSyncUrl(): String = wavelogSyncUrl
|
||||
override fun setWavelogSyncUrl(url: String) { wavelogSyncUrl = url }
|
||||
override fun getLastWavelogSyncEpochMs(): Long = lastWavelogSyncEpochMs
|
||||
override fun setLastWavelogSyncEpochMs(value: Long) { lastWavelogSyncEpochMs = value }
|
||||
|
||||
override fun setSelectedIds(ids: List<Int>) = TODO()
|
||||
override fun setSelectedTypes(types: List<String>) = TODO()
|
||||
override fun setPassesSettings(settings: PassesSettings) = TODO()
|
||||
@@ -293,7 +306,7 @@ class LoTWGridSyncTest {
|
||||
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 updateWavelogUploadSettings(settings: WavelogUploadSettings) = TODO()
|
||||
override fun updateLoTWSettings(settings: LoTWSettings) = TODO()
|
||||
}
|
||||
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Look4Sat-BA7OPF. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2026 BA7OPF.
|
||||
* Based on Look4Sat by 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.logbook.QsoRecord
|
||||
import com.rtbishop.look4sat.core.domain.logbook.QsoStatus
|
||||
import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.DatabaseState
|
||||
import com.rtbishop.look4sat.core.domain.model.GridQso
|
||||
import com.rtbishop.look4sat.core.domain.model.LoTWSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.MarkedStation
|
||||
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.WavelogUploadSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class WavelogSyncTest {
|
||||
|
||||
// region # resolveWavelogSyncMode
|
||||
|
||||
@Test
|
||||
fun fullRequestIsAlwaysHonored() {
|
||||
assertEquals(WavelogSyncMode.Full, resolveWavelogSyncMode("", "http://a", WavelogSyncMode.Full))
|
||||
assertEquals(WavelogSyncMode.Full, resolveWavelogSyncMode("http://a", "http://a", WavelogSyncMode.Full))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun incrementalIsHonoredOnlyOnTheSameServer() {
|
||||
assertEquals(
|
||||
WavelogSyncMode.Incremental,
|
||||
resolveWavelogSyncMode("http://a", "http://a/", WavelogSyncMode.Incremental)
|
||||
)
|
||||
assertEquals(WavelogSyncMode.Full, resolveWavelogSyncMode("", "http://a", WavelogSyncMode.Incremental))
|
||||
assertEquals(
|
||||
WavelogSyncMode.Full,
|
||||
resolveWavelogSyncMode("http://old", "http://a", WavelogSyncMode.Incremental)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun urlNormalizationTrimsWhitespaceAndSlashes() {
|
||||
assertEquals("http://a/wavelog", normalizeWavelogUrl(" http://a/wavelog/ "))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region # buildWavelogGridData
|
||||
|
||||
@Test
|
||||
fun gridDataCollectsTheirGridsAndRoamedOwnGrids() {
|
||||
val data = buildWavelogGridData(
|
||||
listOf(
|
||||
record(start = 1_000L, theirGrid = "EN52", myGrid = "OL62"),
|
||||
record(start = 2_000L, theirGrid = "EN53XX", myGrid = "OL62TI")
|
||||
)
|
||||
)
|
||||
assertEquals(setOf("EN52", "EN53"), data.grids)
|
||||
assertEquals(setOf("OL62"), data.roamedGrids)
|
||||
assertEquals(1, data.gridQsos.getValue("EN52").size)
|
||||
assertEquals(1, data.gridQsos.getValue("EN53").size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gridDataSpreadsAMultiGridQsoAcrossItsGrids() {
|
||||
val data = buildWavelogGridData(
|
||||
listOf(record(start = 1_000L, theirGrid = "EN52", theirVucc = listOf("EN52", "EN53")))
|
||||
)
|
||||
assertEquals(setOf("EN52", "EN53"), data.grids)
|
||||
assertEquals(1, data.gridQsos.getValue("EN52").size)
|
||||
assertEquals(1, data.gridQsos.getValue("EN53").size)
|
||||
// The same QSO object is shared, so its detail matches from either grid.
|
||||
assertEquals(data.gridQsos.getValue("EN52").single(), data.gridQsos.getValue("EN53").single())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gridDataIgnoresNonSatelliteRecordsAndBlankGrids() {
|
||||
val data = buildWavelogGridData(
|
||||
listOf(
|
||||
record(start = 1L, theirGrid = "", sat = ""),
|
||||
record(start = 2L, theirGrid = "XX99", sat = "SO-50")
|
||||
)
|
||||
)
|
||||
assertTrue(data.grids.isEmpty())
|
||||
assertTrue(data.roamedGrids.isEmpty())
|
||||
assertTrue(data.gridQsos.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gridQsoCarriesTheFullRecordDetails() {
|
||||
val data = buildWavelogGridData(listOf(record(start = 1_000L, theirGrid = "EN52", myGrid = "OL62TI")))
|
||||
val qso = data.gridQsos.getValue("EN52").single()
|
||||
assertEquals("BG5JVM", qso.call)
|
||||
assertEquals(1_000L, qso.epochMs)
|
||||
assertEquals("SO-50", qso.satName)
|
||||
assertEquals("FM", qso.mode)
|
||||
assertEquals("2M", qso.bandUp)
|
||||
assertEquals("70CM", qso.bandDown)
|
||||
assertEquals("OL62", qso.myGrid)
|
||||
assertEquals(setOf("OL62"), qso.myGrids)
|
||||
assertEquals(listOf("EN52"), qso.theirGrids)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region # applyWavelogSyncResult
|
||||
|
||||
@Test
|
||||
fun applySyncMergesGridsAndAdvancesBookkeeping() = runBlocking {
|
||||
val settings = FakeSettings()
|
||||
settings.setWorkedGrids(setOf("PM01"))
|
||||
|
||||
val count = applyWavelogSyncResult(
|
||||
settings,
|
||||
listOf(record(start = 1_000L, theirGrid = "EN52", myGrid = "OL62")),
|
||||
mapOf("1" to 500L),
|
||||
"http://a/wavelog/",
|
||||
now = 42L
|
||||
)
|
||||
|
||||
assertEquals(setOf("PM01", "EN52"), settings.getWorkedGrids())
|
||||
assertEquals(setOf("OL62"), settings.getRoamedGrids())
|
||||
assertEquals(setOf("EN52"), settings.getWorkedGridQsos().keys)
|
||||
assertEquals(mapOf("1" to 500L), settings.getWavelogSyncCursors())
|
||||
assertEquals("http://a/wavelog", settings.getWavelogSyncUrl())
|
||||
assertEquals(42L, settings.getLastWavelogSyncEpochMs())
|
||||
assertEquals(2, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun applySyncNeverShrinksStoredData() = runBlocking {
|
||||
val settings = FakeSettings()
|
||||
settings.setWorkedGrids(setOf("PM01"))
|
||||
settings.setRoamedGrids(setOf("OM60"))
|
||||
|
||||
applyWavelogSyncResult(settings, listOf(record(1_000L, "EN52", myGrid = "OL62")), mapOf("1" to 10L), "http://a")
|
||||
applyWavelogSyncResult(settings, listOf(record(2_000L, "EN53", myGrid = "OL63")), mapOf("1" to 20L), "http://a")
|
||||
|
||||
assertEquals(setOf("PM01", "EN52", "EN53"), settings.getWorkedGrids())
|
||||
assertEquals(setOf("OM60", "OL62", "OL63"), settings.getRoamedGrids())
|
||||
assertEquals(mapOf("1" to 20L), settings.getWavelogSyncCursors())
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private fun record(
|
||||
start: Long,
|
||||
theirGrid: String = "",
|
||||
theirVucc: List<String> = emptyList(),
|
||||
myGrid: String = "OL62TI",
|
||||
sat: String = "SO-50"
|
||||
) = QsoRecord(
|
||||
startUtcMillis = start,
|
||||
theirCallsign = "BG5JVM",
|
||||
myCallsign = "BA7OPF",
|
||||
theirGrid = theirGrid,
|
||||
theirVuccGrids = theirVucc,
|
||||
myGrid = myGrid,
|
||||
txFrequencyHz = 145_850_000L,
|
||||
rxFrequencyHz = 436_795_000L,
|
||||
band = "2M",
|
||||
rxBand = "70CM",
|
||||
mode = "FM",
|
||||
satelliteName = sat,
|
||||
propagationMode = if (sat.isNotBlank()) "SAT" else "",
|
||||
status = QsoStatus.COMPLETE
|
||||
)
|
||||
|
||||
private class FakeSettings : ISettingsRepo {
|
||||
private var workedGrids: Set<String> = emptySet()
|
||||
private var workedGridQsos: Map<String, List<GridQso>> = emptyMap()
|
||||
private var roamedGrids: Set<String> = emptySet()
|
||||
private var cursors: Map<String, Long> = emptyMap()
|
||||
private var syncUrl: String = ""
|
||||
private var lastSync: Long = 0L
|
||||
|
||||
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 getWavelogSyncCursors(): Map<String, Long> = cursors
|
||||
override fun setWavelogSyncCursors(cursors: Map<String, Long>) { this.cursors = cursors }
|
||||
override fun getWavelogSyncUrl(): String = syncUrl
|
||||
override fun setWavelogSyncUrl(url: String) { syncUrl = url }
|
||||
override fun getLastWavelogSyncEpochMs(): Long = lastSync
|
||||
override fun setLastWavelogSyncEpochMs(value: Long) { lastSync = value }
|
||||
|
||||
override val appVersionName: String get() = TODO()
|
||||
override val selectedIds: StateFlow<List<Int>> get() = TODO()
|
||||
override val selectedTypes: StateFlow<List<String>> get() = TODO()
|
||||
override fun setSelectedIds(ids: List<Int>) = TODO()
|
||||
override fun setSelectedTypes(types: List<String>) = TODO()
|
||||
override val passesSettings: StateFlow<PassesSettings> get() = TODO()
|
||||
override fun setPassesSettings(settings: PassesSettings) = TODO()
|
||||
override val stationPosition: StateFlow<GeoPos> get() = 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 getCurrentGrid(): String? = TODO()
|
||||
override val databaseState: StateFlow<DatabaseState> get() = 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 val rcSettings: StateFlow<RCSettings> get() = TODO()
|
||||
override fun updateRCSettings(settings: RCSettings) = TODO()
|
||||
override val otherSettings: StateFlow<OtherSettings> get() = TODO()
|
||||
override fun updateOtherSettings(transform: (OtherSettings) -> OtherSettings) = TODO()
|
||||
override val dataSourcesSettings: StateFlow<DataSourcesSettings> get() = TODO()
|
||||
override fun updateDataSourcesSettings(settings: DataSourcesSettings) = TODO()
|
||||
override val dataSourcesStatus: StateFlow<Map<String, Int>> get() = TODO()
|
||||
override fun updateDataSourcesStatus(status: Map<String, Int>) = TODO()
|
||||
override val radioControlSettings: StateFlow<RadioControlSettings> get() = TODO()
|
||||
override fun updateRadioControlSettings(settings: RadioControlSettings) = TODO()
|
||||
override fun getSatelliteOffset(catnum: Int): String = TODO()
|
||||
override fun setSatelliteOffset(catnum: Int, offset: String) = TODO()
|
||||
override fun getSatelliteMode(catnum: Int): String = TODO()
|
||||
override fun setSatelliteMode(catnum: Int, mode: String) = TODO()
|
||||
override fun getAmSatCallsign(): String = TODO()
|
||||
override fun setAmSatCallsign(callsign: String) = TODO()
|
||||
|
||||
override val wavelogUploadSettings: StateFlow<WavelogUploadSettings> get() = TODO()
|
||||
override fun updateWavelogUploadSettings(settings: WavelogUploadSettings) = TODO()
|
||||
override fun getWavelogStations(): List<WavelogStationInfo> = emptyList()
|
||||
override fun setWavelogStations(stations: List<WavelogStationInfo>) = TODO()
|
||||
override fun getMarkedGridStations(): Map<String, List<MarkedStation>> = TODO()
|
||||
override fun setMarkedGridStations(stations: Map<String, List<MarkedStation>>) = TODO()
|
||||
override val lotwSettings: StateFlow<LoTWSettings> get() = TODO()
|
||||
override fun updateLoTWSettings(settings: LoTWSettings) = TODO()
|
||||
override fun getLastLotwSyncDate(): String = TODO()
|
||||
override fun setLastLotwSyncDate(date: String) = TODO()
|
||||
override fun getLastLotwSyncCallsign(): String = TODO()
|
||||
override fun setLastLotwSyncCallsign(callsign: String) = TODO()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user