diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/AdifCodec.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/AdifCodec.kt index 4c1620f5..e48ff990 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/AdifCodec.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/AdifCodec.kt @@ -18,7 +18,7 @@ import java.util.TimeZone object AdifCodec { private val utc = TimeZone.getTimeZone("UTC") - fun encode(records: List): String = buildString { + fun encode(records: List, includeStationCallsign: Boolean = true): String = buildString { append(field("ADIF_VER", "3.1.7")) append(field("PROGRAMID", "Look4Sat")) append("\r\n") @@ -32,7 +32,7 @@ object AdifCodec { append(field("TIME_OFF", format(it, "HHmmss"))) } append(field("CALL", record.theirCallsign)) - append(field("STATION_CALLSIGN", record.myCallsign)) + if (includeStationCallsign) append(field("STATION_CALLSIGN", record.myCallsign)) appendOptional("MODE", record.mode) appendOptional("SUBMODE", record.submode) appendOptional("GRIDSQUARE", record.theirGrid) diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/WavelogUploadMessages.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/WavelogUploadMessages.kt new file mode 100644 index 00000000..40ce51f9 --- /dev/null +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/logbook/WavelogUploadMessages.kt @@ -0,0 +1,66 @@ +/* + * 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 . + */ +package com.rtbishop.look4sat.core.domain.logbook + +import com.rtbishop.look4sat.core.domain.repository.WavelogProblem +import com.rtbishop.look4sat.core.domain.repository.WavelogUploadOutcome +import com.rtbishop.look4sat.core.domain.repository.WavelogUploadPreview + +/** + * The records a Wavelog upload may submit: locally recorded contacts only. + * + * Pure LoTW-imported confirmation rows never carry [QsoRecord.lotwUploaded] (the ADIF decoder + * does not set it), while a locally recorded row keeps its value through a confirmation merge — + * [withConfirmation] leaves `lotwUploaded` untouched. Confirmed local rows uploaded to LoTW + * from this app therefore stay eligible; imported history stays out of the upload. + */ +fun wavelogUploadCandidates(records: List): List = records.filter { + it.status == QsoStatus.COMPLETE && !it.wavelogUploaded && (!it.lotwConfirmed || it.lotwUploaded) +} + +/** Operator-facing wording for why a record cannot enter a Wavelog upload. */ +fun WavelogProblem.label(): String = when (this) { + WavelogProblem.NOT_CONFIGURED -> "Wavelog upload not configured" + WavelogProblem.NO_STATION -> "no station profile selected" + WavelogProblem.GRID_MISMATCH -> "logged grid outside the Wavelog station grid" +} + +/** One-line summary of the records excluded before a Wavelog upload (empty when none). */ +fun wavelogSkipSummary(preview: WavelogUploadPreview): String { + if (preview.skipped <= 0) return "" + val detail = preview.reasons.entries.sortedByDescending { it.value } + .joinToString(", ") { (problem, count) -> "$count× ${problem.label()}" } + return "${preview.skipped} QSO(s) skipped" + if (detail.isNotBlank()) " — $detail" else "" +} + +/** One-line result wording for a finished Wavelog upload. */ +fun wavelogResultMessage(result: WavelogUploadOutcome): String = when (result) { + is WavelogUploadOutcome.Imported -> buildString { + if (result.imported == 0 && result.duplicates > 0) { + append("All ${result.markIds.size} record(s) already in Wavelog") + } else { + append("Uploaded ${result.imported} QSO(s) to Wavelog") + } + if (result.duplicates > 0) append(" — ${result.duplicates} already there") + if (result.skipped > 0) append(" — ${result.skipped} skipped by Wavelog") + } + is WavelogUploadOutcome.Rejected -> result.message + is WavelogUploadOutcome.Failed -> result.message + WavelogUploadOutcome.Expired -> "Preview expired — tap upload again" +} diff --git a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/AdifCodecTest.kt b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/AdifCodecTest.kt index 05a824b7..9a72fa72 100644 --- a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/AdifCodecTest.kt +++ b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/AdifCodecTest.kt @@ -83,4 +83,20 @@ class AdifCodecTest { assertTrue(record.lotwConfirmed) assertTrue(record.isSatellite) } + + @Test + fun encodeOmitsStationCallsignForWavelog() { + val record = QsoRecord( + startUtcMillis = 1_700_000_000_000L, + theirCallsign = "N0CALL", + myCallsign = "BA7OPF", + txFrequencyHz = 145_850_000L, + band = "2M", + satelliteName = "SO-50" + ) + val adif = AdifCodec.encode(listOf(record), includeStationCallsign = false) + assertTrue(!adif.contains("N0CALL")) + assertTrue(adif.contains("SO-50")) + } } diff --git a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/WavelogUploadMessagesTest.kt b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/WavelogUploadMessagesTest.kt new file mode 100644 index 00000000..db962aa0 --- /dev/null +++ b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/logbook/WavelogUploadMessagesTest.kt @@ -0,0 +1,65 @@ +/* + * 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. + */ +package com.rtbishop.look4sat.core.domain.logbook + +import com.rtbishop.look4sat.core.domain.repository.WavelogProblem +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class WavelogUploadMessagesTest { + + private fun record( + complete: Boolean = true, + wavelogUploaded: Boolean = false, + lotwConfirmed: Boolean = false, + lotwUploaded: Boolean = false + ) = QsoRecord( + startUtcMillis = 1_700_000_000_000L, + theirCallsign = "BG5JVM", + myCallsign = "BA7OPF", + status = if (complete) QsoStatus.COMPLETE else QsoStatus.DRAFT, + lotwConfirmed = lotwConfirmed, + lotwUploaded = lotwUploaded, + wavelogUploaded = wavelogUploaded + ) + + @Test + fun freshLocalRecordIsEligible() { + assertTrue(wavelogUploadCandidates(listOf(record())).isNotEmpty()) + } + + @Test + fun locallyUploadedToLotwStaysEligible() { + assertTrue(wavelogUploadCandidates(listOf(record(lotwUploaded = true))).isNotEmpty()) + assertTrue( + wavelogUploadCandidates(listOf(record(lotwConfirmed = true, lotwUploaded = true))).isNotEmpty() + ) + } + + @Test + fun importedConfirmationRowIsExcluded() { + assertTrue(wavelogUploadCandidates(listOf(record(lotwConfirmed = true))).isEmpty()) + } + + @Test + fun draftAndAlreadyUploadedRecordsAreExcluded() { + assertTrue(wavelogUploadCandidates(listOf(record(complete = false))).isEmpty()) + assertTrue(wavelogUploadCandidates(listOf(record(wavelogUploaded = true))).isEmpty()) + } + + @Test + fun problemLabelsAreOperatorFacing() { + assertEquals("Wavelog upload not configured", WavelogProblem.NOT_CONFIGURED.label()) + assertEquals("no station profile selected", WavelogProblem.NO_STATION.label()) + assertEquals("logged grid outside the Wavelog station grid", WavelogProblem.GRID_MISMATCH.label()) + } +}