mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-06 20:47:22 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ea83cb161 | ||
|
|
d7d5918f0a |
No files matched your search
@@ -37,7 +37,12 @@ internal class LoTWSigner(private val config: LoTWConfig) {
|
||||
val call = record.theirCallsign.trim().uppercase(Locale.US)
|
||||
if (record.status != QsoStatus.COMPLETE || !call.matches(Regex("[A-Z0-9]+(/[A-Z0-9]+)*")) ||
|
||||
!call.any(Char::isLetter) || !call.any(Char::isDigit)) fail(LoTWProblem.INVALID_CONTACT, call)
|
||||
if (!record.myCallsign.trim().equals(key.info.callsign, true)) fail(LoTWProblem.CALLSIGN_MISMATCH, call)
|
||||
// Report the two callsigns the operator has to reconcile (record vs certificate). A generic
|
||||
// sample — the opposite station's call — says nothing about which side is wrong, which is
|
||||
// exactly what a "MY callsign does not match the certificate" message must tell.
|
||||
if (!record.myCallsign.trim().equals(key.info.callsign, true)) {
|
||||
fail(LoTWProblem.CALLSIGN_MISMATCH, "${record.myCallsign.trim()} ≠ ${key.info.callsign}")
|
||||
}
|
||||
val date = utc(record.startUtcMillis, "yyyy-MM-dd")
|
||||
// A record dated after "now" gets its own message: it happens when the operator
|
||||
// logged against a pass that had not started yet (out-of-window flow), and "fix the
|
||||
|
||||
+50
@@ -22,6 +22,7 @@ import com.rtbishop.look4sat.core.domain.logbook.QsoStatus
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWOperationException
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWProblem
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWStation
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -120,6 +121,55 @@ class LoTWUploadRepositoryPreviewTest {
|
||||
}
|
||||
}
|
||||
|
||||
/** Nothing signable at all: the preview still comes back (count 0) with the conflicts listed,
|
||||
* so the UI can offer the rewrite / use-another-certificate actions instead of a dead end. */
|
||||
@Test
|
||||
fun `fully blocked batch still returns a preview with the conflicts`() {
|
||||
runBlocking {
|
||||
val storage = MemStorage()
|
||||
val repo = LoTWUploadRepository(
|
||||
storage,
|
||||
{ LoTWConfig(File("src/main/assets/lotw/config.tq6").inputStream()) },
|
||||
now = { System.currentTimeMillis() }
|
||||
)
|
||||
repo.importCertificate(fixture("test_tqsl_empty.p12"), charArrayOf())
|
||||
repo.saveStation(LoTWStation(grid = "OL62TI"))
|
||||
val start = 1_787_000_000_000L
|
||||
val preview = repo.prepare(
|
||||
listOf(
|
||||
sampleRecord(start, "BH6RJD", "XX0YY"),
|
||||
sampleRecord(start + 60_000L, "BG7QBL", "XX0ZZ")
|
||||
),
|
||||
resubmit = false
|
||||
)
|
||||
assertEquals(0, preview.count)
|
||||
assertEquals(2, preview.unavailableSkipped)
|
||||
assertEquals(listOf(start, start + 60_000L), preview.callsignConflicts)
|
||||
assertEquals(2, preview.unavailableReasons[LoTWProblem.CALLSIGN_MISMATCH])
|
||||
assertEquals(0, preview.missingCallsign)
|
||||
// No payload was prepared: confirming this preview must fail as expired, not crash.
|
||||
assertTrue(repo.upload(preview.id) is LoTWUploadResult.ExpiredPreview)
|
||||
}
|
||||
}
|
||||
|
||||
/** The mismatch reason names both callsigns (record vs certificate). The old detail showed the
|
||||
* opposite station's call, which told the operator nothing about which side to fix. */
|
||||
@Test
|
||||
fun `callsign mismatch detail names the record and the certificate callsign`() {
|
||||
runBlocking {
|
||||
val storage = MemStorage()
|
||||
val repo = LoTWUploadRepository(
|
||||
storage,
|
||||
{ LoTWConfig(File("src/main/assets/lotw/config.tq6").inputStream()) },
|
||||
now = { System.currentTimeMillis() }
|
||||
)
|
||||
repo.importCertificate(fixture("test_tqsl_empty.p12"), charArrayOf())
|
||||
repo.saveStation(LoTWStation(grid = "OL62TI"))
|
||||
val audit = repo.audit(listOf(sampleRecord(1_787_000_000_000L, "BH6RJD", "XX0YY")))
|
||||
assertEquals("XX0YY ≠ BA7OPF", audit.details[LoTWProblem.CALLSIGN_MISMATCH])
|
||||
}
|
||||
}
|
||||
|
||||
private fun sampleRecord(start: Long, call: String, myCallsign: String) = QsoRecord(
|
||||
id = start,
|
||||
startUtcMillis = start,
|
||||
|
||||
+12
@@ -18,6 +18,7 @@
|
||||
package com.rtbishop.look4sat.core.domain.logbook
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWProblem
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadAudit
|
||||
|
||||
/**
|
||||
* Wording for the reasons a QSO cannot be signed for LoTW.
|
||||
@@ -69,3 +70,14 @@ fun unavailableUploadSummary(
|
||||
val detail = parts.joinToString(", ").ifBlank { "check the logbook" }
|
||||
return "$subject — $detail"
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a batch that has nothing uploadable still has to reach the preview dialog.
|
||||
*
|
||||
* The preview is the only place that carries the callsign-conflict actions (rewrite the records, or
|
||||
* upload them under another certificate). Short-circuiting such a batch into the plain "can't be
|
||||
* uploaded" message strands exactly the records the operator needs to act on — the message has no
|
||||
* buttons to get out of, so those QSOs could never be uploaded.
|
||||
*/
|
||||
fun LoTWUploadAudit.needsPreviewForConflicts(): Boolean =
|
||||
pending == 0 && (reasons[LoTWProblem.CALLSIGN_MISMATCH] ?: 0) > 0
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.logbook
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWProblem
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadAudit
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* The preview dialog is the only place carrying the callsign-conflict actions (rewrite / use another
|
||||
* certificate), so a conflict-only batch must NOT be short-circuited into the plain
|
||||
* "can't be uploaded" message: that dialog has no buttons to get out of.
|
||||
*/
|
||||
class LoTWUploadMessagesTest {
|
||||
|
||||
@Test
|
||||
fun `conflict-only batch asks for the preview even with nothing pending`() {
|
||||
assertTrue(audit(pending = 0, mismatch = 107, satellite = 1).needsPreviewForConflicts())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocked batch without conflicts keeps the plain message`() {
|
||||
assertFalse(audit(pending = 0, mismatch = 0, satellite = 3).needsPreviewForConflicts())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `batch with something to upload never takes the conflict fallback`() {
|
||||
assertFalse(audit(pending = 5, mismatch = 2, satellite = 1).needsPreviewForConflicts())
|
||||
}
|
||||
|
||||
private fun audit(pending: Int, mismatch: Int, satellite: Int) = LoTWUploadAudit(
|
||||
total = 108,
|
||||
pending = pending,
|
||||
uploaded = 0,
|
||||
unknown = 0,
|
||||
unavailable = 108 - pending,
|
||||
reasons = buildMap {
|
||||
if (mismatch > 0) put(LoTWProblem.CALLSIGN_MISMATCH, mismatch)
|
||||
if (satellite > 0) put(LoTWProblem.SATELLITE, satellite)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -431,7 +431,10 @@ private fun UploadPreviewDialog(
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text("${preview.callsign} DXCC ${preview.dxcc} Grid ${preview.grid}", fontSize = 13.sp)
|
||||
Text("${preview.count} QSO(s) · ${
|
||||
val range = if (preview.firstUtc.isBlank()) {
|
||||
// A blocked batch (shown for its conflict actions) has no date range to show.
|
||||
"${preview.count} QSO(s)"
|
||||
} else "${preview.count} QSO(s) · ${
|
||||
java.text.SimpleDateFormat("MM-dd HH:mm", java.util.Locale.US).apply {
|
||||
timeZone = java.util.TimeZone.getTimeZone("UTC")
|
||||
}.format(java.util.Date(preview.firstUtc.let {
|
||||
@@ -447,7 +450,8 @@ private fun UploadPreviewDialog(
|
||||
timeZone = java.util.TimeZone.getTimeZone("UTC")
|
||||
}.parse(it).time }.getOrDefault(System.currentTimeMillis())
|
||||
}))
|
||||
}Z", fontSize = 13.sp)
|
||||
}Z"
|
||||
Text(range, fontSize = 13.sp)
|
||||
if (preview.skipped > 0 || preview.unknownSkipped > 0 || preview.unavailableSkipped > 0) {
|
||||
val parts = buildList {
|
||||
// skipped counts every record left out of this batch: previously-uploaded
|
||||
@@ -498,7 +502,8 @@ private fun UploadPreviewDialog(
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm, enabled = !busy) { Text("确认上传") }
|
||||
// Nothing uploadable (a conflict-only batch): the actions above are the way out.
|
||||
TextButton(onClick = onConfirm, enabled = !busy && preview.count > 0) { Text("确认上传") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.rtbishop.look4sat.core.domain.logbook.QsoRecord
|
||||
import com.rtbishop.look4sat.core.domain.logbook.QsoStatus
|
||||
import com.rtbishop.look4sat.core.domain.logbook.displayMode
|
||||
import com.rtbishop.look4sat.core.domain.logbook.frequencyBand
|
||||
import com.rtbishop.look4sat.core.domain.logbook.needsPreviewForConflicts
|
||||
import com.rtbishop.look4sat.core.domain.logbook.officialSatelliteName
|
||||
import com.rtbishop.look4sat.core.domain.logbook.satelliteIdentity
|
||||
import com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary
|
||||
@@ -286,7 +287,10 @@ class LogViewModel(
|
||||
// LoTW-imported confirmations are the feedback side.
|
||||
val pending = all.filter { !it.lotwConfirmed && !it.lotwUploaded && it.status == QsoStatus.COMPLETE }
|
||||
val audit = lotwUploadRepository.audit(pending)
|
||||
if (audit.pending == 0) {
|
||||
// A batch held back only by callsign conflicts still gets the preview: that dialog
|
||||
// carries the rewrite / use-another-certificate actions. Without this the operator
|
||||
// gets a dead-end message and those records can never be uploaded.
|
||||
if (audit.pending == 0 && !audit.needsPreviewForConflicts()) {
|
||||
val msg = when {
|
||||
audit.unavailable > 0 -> unavailableUploadSummary(
|
||||
audit.unavailable, audit.reasons, audit.details, audit.duplicates, audit.incomplete
|
||||
|
||||
+6
-2
@@ -299,7 +299,9 @@ private fun LogbookUploadPreviewDialog(
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text("${preview.callsign} DXCC ${preview.dxcc} Grid ${preview.grid}", fontSize = 13.sp)
|
||||
Text(
|
||||
"${preview.count} QSO(s) · ${preview.firstUtc} – ${preview.lastUtc}",
|
||||
// A blocked batch (shown for its conflict actions) has no date range to show.
|
||||
if (preview.firstUtc.isBlank()) "${preview.count} QSO(s)"
|
||||
else "${preview.count} QSO(s) · ${preview.firstUtc} – ${preview.lastUtc}",
|
||||
fontSize = 13.sp
|
||||
)
|
||||
if (preview.skipped > 0 || preview.unknownSkipped > 0 || preview.unavailableSkipped > 0) {
|
||||
@@ -357,7 +359,9 @@ private fun LogbookUploadPreviewDialog(
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm, enabled = !busy) {
|
||||
// Nothing uploadable (a conflict-only batch): the actions above are the way out, so the
|
||||
// upload button stays disabled instead of failing with an expired preview.
|
||||
TextButton(onClick = onConfirm, enabled = !busy && preview.count > 0) {
|
||||
Text(stringResource(R.string.prefs_logbook_upload_confirm))
|
||||
}
|
||||
},
|
||||
|
||||
+5
-1
@@ -46,6 +46,7 @@ import com.rtbishop.look4sat.core.domain.repository.WavelogUploadOutcome
|
||||
import com.rtbishop.look4sat.core.domain.repository.WavelogUploadPreview
|
||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||
import com.rtbishop.look4sat.core.domain.utility.VersionComparator
|
||||
import com.rtbishop.look4sat.core.domain.logbook.needsPreviewForConflicts
|
||||
import com.rtbishop.look4sat.core.domain.logbook.resubmitCandidates
|
||||
import com.rtbishop.look4sat.core.domain.logbook.toConfirmedRecord
|
||||
import com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary
|
||||
@@ -576,7 +577,10 @@ class SettingsViewModel(
|
||||
// LoTW-imported confirmations are the feedback side.
|
||||
val pending = all.filter { !it.lotwConfirmed && !it.lotwUploaded && it.status == com.rtbishop.look4sat.core.domain.logbook.QsoStatus.COMPLETE }
|
||||
val audit = lotwUploadRepository.audit(pending)
|
||||
if (audit.pending == 0) {
|
||||
// A batch held back only by callsign conflicts still gets the preview: that dialog
|
||||
// carries the rewrite / use-another-certificate actions. Without this the operator
|
||||
// gets a dead-end message and those records can never be uploaded.
|
||||
if (audit.pending == 0 && !audit.needsPreviewForConflicts()) {
|
||||
val msg = when {
|
||||
audit.unavailable > 0 -> com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary(
|
||||
audit.unavailable, audit.reasons, audit.details, audit.duplicates, audit.incomplete
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionCode = "578"
|
||||
appVersionCode = "579"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.4.8-ba7opf.11.3"
|
||||
appVersionName = "4.4.8-ba7opf.11.4"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compileSdk = "37"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
|
||||
Reference in new issue
Block a user