mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-08 21:47:23 +00:00
fix(lotw): 空呼号记录按证书上传并回填,呼号冲突可选改写或换证书
证书装好之前记录的 QSO 自己呼号存空,签名时被判 CALLSIGN_MISMATCH,整批上传失败。 现在这类记录改用当前证书呼号签名,不再拒签:预览提示「其中 N 条记录没有呼号,将按证书 XXX 上传」,上传成功后仅把这些记录的空呼号回填为证书呼号(只填空值,经 save() 写入以 保持去重键一致,不动时间/频率/模式/对方呼号/上传状态与确认状态)。 自己呼号非空且与证书不同的记录仍拒签(不冒签他人记录),但预览给出两个选择: 「改写为 XXX」把冲突记录的自己呼号改成当前证书呼号后即可重传;「换一张证书」在设置页 关掉日志本并打开 LoTW 配置导入另一张证书(雷达页提示去设置页导入)。 新增单测:空呼号签名与计数、冲突记录 id、markUploaded 只填空值、rewriteMyCallsign 只改 冲突记录;并修正测试夹具 MemStorage.read 返回共享数组(被调用方 fill(0) 后破坏已存证书, 与文件存储行为不一致)导致的问题。
This commit is contained in:
14 files changed
+289
-17
No files matched your search
@@ -262,7 +262,9 @@ fun LogPage(
|
||||
preview = preview,
|
||||
busy = logUiState.busy,
|
||||
onConfirm = logViewModel::confirmUpload,
|
||||
onDismiss = logViewModel::dismissPreview
|
||||
onDismiss = logViewModel::dismissPreview,
|
||||
onRewriteCallsign = logViewModel::rewriteCallsignConflicts,
|
||||
onSwitchCertificate = logViewModel::switchCertificateForConflicts
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -417,7 +419,9 @@ private fun UploadPreviewDialog(
|
||||
preview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview,
|
||||
busy: Boolean,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
onDismiss: () -> Unit,
|
||||
onRewriteCallsign: () -> Unit,
|
||||
onSwitchCertificate: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -464,6 +468,32 @@ private fun UploadPreviewDialog(
|
||||
}
|
||||
Text(parts.joinToString(" · "), fontSize = 12.sp, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
if (preview.missingCallsign > 0) {
|
||||
// Logged while no certificate was installed; signed with the certificate's
|
||||
// callsign instead of being refused.
|
||||
Text(
|
||||
stringResource(R.string.prefs_logbook_callsign_missing, preview.missingCallsign, preview.callsign),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
if (preview.callsignConflicts.isNotEmpty()) {
|
||||
// These name another callsign than the certificate: rewrite them with this
|
||||
// certificate's callsign, or upload them under the other certificate.
|
||||
Text(
|
||||
stringResource(R.string.prefs_logbook_callsign_different, preview.callsignConflicts.size, preview.callsign),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
TextButton(onClick = onRewriteCallsign, enabled = !busy) {
|
||||
Text(stringResource(R.string.prefs_logbook_callsign_rewrite, preview.callsign), fontSize = 12.sp)
|
||||
}
|
||||
TextButton(onClick = onSwitchCertificate, enabled = !busy) {
|
||||
Text(stringResource(R.string.prefs_logbook_callsign_switch), fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(preview.contacts.joinToString("\n") { it }, fontSize = 12.sp, maxLines = 8)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -343,7 +343,7 @@ class LogViewModel(
|
||||
if (result is LoTWUploadResult.Accepted && lastUploadedIds.isNotEmpty()) {
|
||||
// Mark the submitted QSOs as uploaded (distinct from confirmed) and stamp
|
||||
// the station grids this batch went out under.
|
||||
qsoRepository.markUploaded(lastUploadedIds, preview.grids)
|
||||
qsoRepository.markUploaded(lastUploadedIds, preview.grids, preview.callsign)
|
||||
lastUploadedIds = emptyList()
|
||||
}
|
||||
val msg = when (result) {
|
||||
@@ -368,6 +368,34 @@ class LogViewModel(
|
||||
|
||||
fun dismissPreview() = _uiState.update { it.copy(preview = null) }
|
||||
|
||||
/**
|
||||
* Operator chose "rewrite" on the callsign conflict: the refused records get this certificate's
|
||||
* callsign so the next upload can sign them. The prepared batch is dropped — nothing was sent.
|
||||
*/
|
||||
fun rewriteCallsignConflicts() {
|
||||
val preview = _uiState.value.preview ?: return
|
||||
val ids = preview.callsignConflicts
|
||||
if (ids.isEmpty()) return
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(busy = true) }
|
||||
val changed = qsoRepository.rewriteMyCallsign(ids, preview.callsign)
|
||||
lotwUploadRepository.discardPreview()
|
||||
lastUploadedIds = emptyList()
|
||||
_uiState.update {
|
||||
it.copy(busy = false, preview = null, message = "$changed record(s) rewritten to ${preview.callsign} — upload again")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Operator chose "use another certificate": that certificate is imported on the settings page. */
|
||||
fun switchCertificateForConflicts() {
|
||||
lotwUploadRepository.discardPreview()
|
||||
lastUploadedIds = emptyList()
|
||||
_uiState.update {
|
||||
it.copy(preview = null, message = "Import the other callsign's certificate in Settings → LoTW upload, then upload again")
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissWavelogPreview() = _uiState.update { it.copy(wavelogPreview = null) }
|
||||
|
||||
/**
|
||||
|
||||
+36
-2
@@ -103,6 +103,10 @@ fun LogbookDialog(
|
||||
onDismissMessage: () -> Unit,
|
||||
onIgnorePositionWarning: () -> Unit = {},
|
||||
onFixGrid: (List<String>) -> Unit = {},
|
||||
/** Rewrite the records whose own callsign differs from the certificate's. */
|
||||
onRewriteCallsign: () -> Unit = {},
|
||||
/** Leave the upload to import a different certificate instead. */
|
||||
onSwitchCertificate: () -> Unit = {},
|
||||
selectionMode: Boolean = false,
|
||||
selectedIds: Set<Long> = emptySet(),
|
||||
onStartSelection: (Long) -> Unit = {},
|
||||
@@ -246,7 +250,9 @@ fun LogbookDialog(
|
||||
preview = preview,
|
||||
busy = uploadBusy,
|
||||
onConfirm = onConfirmUpload,
|
||||
onDismiss = onDismissPreview
|
||||
onDismiss = onDismissPreview,
|
||||
onRewriteCallsign = onRewriteCallsign,
|
||||
onSwitchCertificate = onSwitchCertificate
|
||||
)
|
||||
}
|
||||
if (uploadMessage.isNotBlank()) {
|
||||
@@ -280,7 +286,9 @@ private fun LogbookUploadPreviewDialog(
|
||||
preview: com.rtbishop.look4sat.core.domain.repository.LoTWUploadPreview,
|
||||
busy: Boolean,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
onDismiss: () -> Unit,
|
||||
onRewriteCallsign: () -> Unit,
|
||||
onSwitchCertificate: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -319,6 +327,32 @@ private fun LogbookUploadPreviewDialog(
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
if (preview.missingCallsign > 0) {
|
||||
// Logged while no certificate was installed; signed with the certificate's
|
||||
// callsign instead of being refused.
|
||||
Text(
|
||||
stringResource(R.string.prefs_logbook_callsign_missing, preview.missingCallsign, preview.callsign),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
if (preview.callsignConflicts.isNotEmpty()) {
|
||||
// These name another callsign than the certificate: the operator either rewrites
|
||||
// them with the certificate's callsign or uploads them under that other one.
|
||||
Text(
|
||||
stringResource(R.string.prefs_logbook_callsign_different, preview.callsignConflicts.size, preview.callsign),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
TextButton(onClick = onRewriteCallsign, enabled = !busy) {
|
||||
Text(stringResource(R.string.prefs_logbook_callsign_rewrite, preview.callsign), fontSize = 12.sp)
|
||||
}
|
||||
TextButton(onClick = onSwitchCertificate, enabled = !busy) {
|
||||
Text(stringResource(R.string.prefs_logbook_callsign_switch), fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(preview.contacts.joinToString("\n") { it }, fontSize = 12.sp, maxLines = 8)
|
||||
}
|
||||
},
|
||||
|
||||
+9
@@ -308,6 +308,15 @@ private fun SettingsScreen(
|
||||
onConfirmUpload = { onAction(SettingsAction.ConfirmLogbookUpload) },
|
||||
onDismissPreview = { onAction(SettingsAction.DismissLogbookPreview) },
|
||||
onDismissMessage = { onAction(SettingsAction.ClearLogbookMessage) },
|
||||
onRewriteCallsign = { onAction(SettingsAction.RewriteLogbookCallsigns) },
|
||||
onSwitchCertificate = {
|
||||
// The other callsign's certificate lives in the LoTW upload settings: leave the
|
||||
// logbook and open that dialog.
|
||||
onAction(SettingsAction.DismissLogbookPreview)
|
||||
onAction(SettingsAction.ExitLogbookSelection)
|
||||
dialogs.logbook = false
|
||||
dialogs.lotwUpload = true
|
||||
},
|
||||
onIgnorePositionWarning = { onAction(SettingsAction.IgnoreLogbookPositionWarning) },
|
||||
onFixGrid = { grids ->
|
||||
onAction(SettingsAction.AbandonLogbookForGridFix)
|
||||
|
||||
@@ -193,6 +193,7 @@ sealed interface SettingsAction {
|
||||
data object PrepareLogbookUpload : SettingsAction
|
||||
data object ConfirmLogbookUpload : SettingsAction
|
||||
data object DismissLogbookPreview : SettingsAction
|
||||
data object RewriteLogbookCallsigns : SettingsAction
|
||||
/** The operator acknowledged the position mismatch and wants to upload anyway. */
|
||||
data object IgnoreLogbookPositionWarning : SettingsAction
|
||||
/** The operator chose to fix the station location first; the preview is discarded. */
|
||||
|
||||
+25
-1
@@ -257,6 +257,7 @@ class SettingsViewModel(
|
||||
SettingsAction.PrepareLogbookUpload -> prepareLogbookUpload()
|
||||
SettingsAction.ConfirmLogbookUpload -> confirmLogbookUpload()
|
||||
SettingsAction.DismissLogbookPreview -> dismissLogbookPreview()
|
||||
SettingsAction.RewriteLogbookCallsigns -> rewriteLogbookCallsigns()
|
||||
SettingsAction.IgnoreLogbookPositionWarning -> ignoreLogbookPositionWarning()
|
||||
SettingsAction.AbandonLogbookForGridFix -> abandonLogbookForGridFix()
|
||||
is SettingsAction.StartLogbookSelection -> startLogbookSelection(action.id)
|
||||
@@ -648,7 +649,7 @@ class SettingsViewModel(
|
||||
val result = lotwUploadRepository.upload(preview.id)
|
||||
val accepted = result is com.rtbishop.look4sat.core.domain.repository.LoTWUploadResult.Accepted
|
||||
if (accepted && lastLogbookUploadIds.isNotEmpty()) {
|
||||
qsoRepository.markUploaded(lastLogbookUploadIds, preview.grids)
|
||||
qsoRepository.markUploaded(lastLogbookUploadIds, preview.grids, preview.callsign)
|
||||
lastLogbookUploadIds = emptyList()
|
||||
}
|
||||
val msg = when (result) {
|
||||
@@ -673,6 +674,29 @@ class SettingsViewModel(
|
||||
|
||||
private fun dismissLogbookPreview() = _uiState.update { it.copy(logbookPreview = null, logbookWavelogPreview = null) }
|
||||
|
||||
/**
|
||||
* Operator chose "rewrite" on the callsign conflict: the refused records get this certificate's
|
||||
* callsign so the next upload can sign them. The batch is discarded — nothing was uploaded.
|
||||
*/
|
||||
private fun rewriteLogbookCallsigns() {
|
||||
val preview = _uiState.value.logbookPreview ?: return
|
||||
val ids = preview.callsignConflicts
|
||||
if (ids.isEmpty()) return
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(logbookUploadBusy = true) }
|
||||
val changed = qsoRepository.rewriteMyCallsign(ids, preview.callsign)
|
||||
lotwUploadRepository.discardPreview()
|
||||
lastLogbookUploadIds = emptyList()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
logbookUploadBusy = false,
|
||||
logbookPreview = null,
|
||||
logbookUploadMessage = "$changed record(s) rewritten to ${preview.callsign} — upload again"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Operator chose "ignore" on the position check: keep the prepared preview. */
|
||||
private fun ignoreLogbookPositionWarning() = _uiState.update { it.copy(logbookPositionWarning = null) }
|
||||
|
||||
|
||||
Reference in new issue
Block a user