mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-07 04:57:23 +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)
|
val call = record.theirCallsign.trim().uppercase(Locale.US)
|
||||||
if (record.status != QsoStatus.COMPLETE || !call.matches(Regex("[A-Z0-9]+(/[A-Z0-9]+)*")) ||
|
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)
|
!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")
|
val date = utc(record.startUtcMillis, "yyyy-MM-dd")
|
||||||
// A record dated after "now" gets its own message: it happens when the operator
|
// 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
|
// 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.LoTWOperationException
|
||||||
import com.rtbishop.look4sat.core.domain.repository.LoTWProblem
|
import com.rtbishop.look4sat.core.domain.repository.LoTWProblem
|
||||||
import com.rtbishop.look4sat.core.domain.repository.LoTWStation
|
import com.rtbishop.look4sat.core.domain.repository.LoTWStation
|
||||||
|
import com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert.assertEquals
|
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(
|
private fun sampleRecord(start: Long, call: String, myCallsign: String) = QsoRecord(
|
||||||
id = start,
|
id = start,
|
||||||
startUtcMillis = start,
|
startUtcMillis = start,
|
||||||
|
|||||||
+12
@@ -18,6 +18,7 @@
|
|||||||
package com.rtbishop.look4sat.core.domain.logbook
|
package com.rtbishop.look4sat.core.domain.logbook
|
||||||
|
|
||||||
import com.rtbishop.look4sat.core.domain.repository.LoTWProblem
|
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.
|
* 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" }
|
val detail = parts.joinToString(", ").ifBlank { "check the logbook" }
|
||||||
return "$subject — $detail"
|
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 = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
Text("${preview.callsign} DXCC ${preview.dxcc} Grid ${preview.grid}", fontSize = 13.sp)
|
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 {
|
java.text.SimpleDateFormat("MM-dd HH:mm", java.util.Locale.US).apply {
|
||||||
timeZone = java.util.TimeZone.getTimeZone("UTC")
|
timeZone = java.util.TimeZone.getTimeZone("UTC")
|
||||||
}.format(java.util.Date(preview.firstUtc.let {
|
}.format(java.util.Date(preview.firstUtc.let {
|
||||||
@@ -447,7 +450,8 @@ private fun UploadPreviewDialog(
|
|||||||
timeZone = java.util.TimeZone.getTimeZone("UTC")
|
timeZone = java.util.TimeZone.getTimeZone("UTC")
|
||||||
}.parse(it).time }.getOrDefault(System.currentTimeMillis())
|
}.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) {
|
if (preview.skipped > 0 || preview.unknownSkipped > 0 || preview.unavailableSkipped > 0) {
|
||||||
val parts = buildList {
|
val parts = buildList {
|
||||||
// skipped counts every record left out of this batch: previously-uploaded
|
// skipped counts every record left out of this batch: previously-uploaded
|
||||||
@@ -498,7 +502,8 @@ private fun UploadPreviewDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
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 = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismiss) { Text("Cancel") }
|
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.QsoStatus
|
||||||
import com.rtbishop.look4sat.core.domain.logbook.displayMode
|
import com.rtbishop.look4sat.core.domain.logbook.displayMode
|
||||||
import com.rtbishop.look4sat.core.domain.logbook.frequencyBand
|
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.officialSatelliteName
|
||||||
import com.rtbishop.look4sat.core.domain.logbook.satelliteIdentity
|
import com.rtbishop.look4sat.core.domain.logbook.satelliteIdentity
|
||||||
import com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary
|
import com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary
|
||||||
@@ -286,7 +287,10 @@ class LogViewModel(
|
|||||||
// LoTW-imported confirmations are the feedback side.
|
// LoTW-imported confirmations are the feedback side.
|
||||||
val pending = all.filter { !it.lotwConfirmed && !it.lotwUploaded && it.status == QsoStatus.COMPLETE }
|
val pending = all.filter { !it.lotwConfirmed && !it.lotwUploaded && it.status == QsoStatus.COMPLETE }
|
||||||
val audit = lotwUploadRepository.audit(pending)
|
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 {
|
val msg = when {
|
||||||
audit.unavailable > 0 -> unavailableUploadSummary(
|
audit.unavailable > 0 -> unavailableUploadSummary(
|
||||||
audit.unavailable, audit.reasons, audit.details, audit.duplicates, audit.incomplete
|
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)) {
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
Text("${preview.callsign} DXCC ${preview.dxcc} Grid ${preview.grid}", fontSize = 13.sp)
|
Text("${preview.callsign} DXCC ${preview.dxcc} Grid ${preview.grid}", fontSize = 13.sp)
|
||||||
Text(
|
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
|
fontSize = 13.sp
|
||||||
)
|
)
|
||||||
if (preview.skipped > 0 || preview.unknownSkipped > 0 || preview.unavailableSkipped > 0) {
|
if (preview.skipped > 0 || preview.unknownSkipped > 0 || preview.unavailableSkipped > 0) {
|
||||||
@@ -357,7 +359,9 @@ private fun LogbookUploadPreviewDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
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))
|
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.repository.WavelogUploadPreview
|
||||||
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
import com.rtbishop.look4sat.core.domain.usecase.IShowToast
|
||||||
import com.rtbishop.look4sat.core.domain.utility.VersionComparator
|
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.resubmitCandidates
|
||||||
import com.rtbishop.look4sat.core.domain.logbook.toConfirmedRecord
|
import com.rtbishop.look4sat.core.domain.logbook.toConfirmedRecord
|
||||||
import com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary
|
import com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary
|
||||||
@@ -576,7 +577,10 @@ class SettingsViewModel(
|
|||||||
// LoTW-imported confirmations are the feedback side.
|
// 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 pending = all.filter { !it.lotwConfirmed && !it.lotwUploaded && it.status == com.rtbishop.look4sat.core.domain.logbook.QsoStatus.COMPLETE }
|
||||||
val audit = lotwUploadRepository.audit(pending)
|
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 {
|
val msg = when {
|
||||||
audit.unavailable > 0 -> com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary(
|
audit.unavailable > 0 -> com.rtbishop.look4sat.core.domain.logbook.unavailableUploadSummary(
|
||||||
audit.unavailable, audit.reasons, audit.details, audit.duplicates, audit.incomplete
|
audit.unavailable, audit.reasons, audit.details, audit.duplicates, audit.incomplete
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
appVersionCode = "578"
|
appVersionCode = "579"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
appVersionName = "4.4.8-ba7opf.11.3"
|
appVersionName = "4.4.8-ba7opf.11.4"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
compileSdk = "37"
|
compileSdk = "37"
|
||||||
#noinspection UnusedVersionCatalogEntry
|
#noinspection UnusedVersionCatalogEntry
|
||||||
|
|||||||
Reference in new issue
Block a user