From 8445c17033286f641864e341f54aaacf9122b56f Mon Sep 17 00:00:00 2001 From: QIU Date: Thu, 27 Aug 2026 07:31:20 +0000 Subject: [PATCH] fix: remove the receive-only notice, and stop the CW record scrolling itself Two things the operator asked for after running 4.6.1. The receive-only notice named a state this app does not have. APRS-IS lets an unverified station connect and then discards its packets, which is what "receive-only" means at the protocol level - but this app only reports its own position. There is no receiving side to it, and none intended, so telling the operator they are in receive-only mode described a mode that does not exist here. Without a passcode the packet does not arrive, and the unverified notice already says exactly that. The string is gone from all five locales, along with the AprsReport.receiveOnly field, which had no remaining consumer. AprsPasscode.classify stays: loginValue still uses it, and its tests hold the distinction between a deliberate -1 and a typo, which is a separate defect worth keeping fixed. The CW history pane no longer follows the decode. Its whole purpose is to be read back, and a record that scrolls itself is worse than paper - as the operator put it, if it scrolls away then why use a decoder instead of listening and writing it down, since paper does not erase itself. The single line above it is where new characters appear; that still scrolls, because that is its job. A down arrow in the toolbar jumps to the newest text when wanted. Not fixed here: logged times in the log page look wrong and inconsistent. I proposed a timezone explanation and wrote a probe, and the probe disproved it - on a real JVM both the session header and the row times are stable and both resolve to local time. That reverted attempt is not in this commit. The cause is still unknown. --- .../look4sat/AprsForegroundService.kt | 10 ++-- .../look4sat/core/data/aprs/AprsReporter.kt | 12 +---- .../src/main/res/values-id/strings.xml | 1 - .../src/main/res/values-in/strings.xml | 1 - .../src/main/res/values-tr/strings.xml | 1 - .../src/main/res/values-zh/strings.xml | 1 - .../src/main/res/values/strings.xml | 1 - .../look4sat/feature/cw/CwDecodeScreen.kt | 50 +++++++++---------- .../cw/src/main/res/values-id/app_values.xml | 1 + .../cw/src/main/res/values-in/app_values.xml | 1 + .../cw/src/main/res/values-tr/app_values.xml | 1 + .../cw/src/main/res/values-zh/app_values.xml | 1 + feature/cw/src/main/res/values/app_values.xml | 1 + 13 files changed, 37 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt index 52cf08a5..7f287051 100644 --- a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt +++ b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt @@ -123,13 +123,13 @@ class AprsForegroundService : Service() { // An unverified login needs its own message: the write succeeded, so a bare // failure notice would send the operator looking at their network when the // problem is the passcode - and APRS-IS is dropping every packet meanwhile. + // No receive-only notice. APRS-IS lets an unverified station connect and then + // discards its packets, but this app only reports its own position - there is no + // receiving side to it - so telling the operator they are "in receive-only mode" + // named a state that does not exist here. Without a passcode the packet does not + // arrive, and that is what the failure notice says. val msg = when { report.ok -> getString(R.string.aprs_toast_ok) - // Receive-only first: it logs in with -1 exactly as a wrong passcode does and - // the server answers "unverified" to both, so without this branch the one safe - // way to test a setup reported itself as a configuration error. - !report.verified && report.receiveOnly -> - getString(R.string.aprs_toast_receive_only) !report.verified -> getString(R.string.aprs_toast_unverified) else -> getString(R.string.aprs_toast_fail, report.detail) } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt index ed4fa666..67b39c24 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt @@ -52,7 +52,7 @@ data class AprsReport( * test a setup without putting anything on the network - was reported as a wrong passcode * and sent the operator to fix something they had set on purpose. */ - val receiveOnly: Boolean = false + ) /** Report scheduler (periodic + manual trigger); connection management lives in the foreground service */ @@ -127,13 +127,6 @@ class AprsReporter( } val packetLine = (beacon as AprsBeacon.Result.Line).text - // Receive-only is a deliberate choice and has to be told apart from a typo, because - // both log in with -1 and the server answers "unverified" to each. Classifying rather - // than asking canTransmit: that collapses ReceiveOnly and Mismatch into one boolean, - // so a mistyped passcode would be told it is in receive-only mode - the same - // mis-diagnosis as before, pointing the other way. - val wantsReceiveOnly = - AprsPasscode.classify(cfg.callsign, cfg.passcode) is AprsPasscode.Entry.ReceiveOnly val c = client ?: AprsIsClient( host = cfg.server, port = cfg.port, @@ -166,14 +159,13 @@ class AprsReporter( // report, say that instead. val reported = when { ok -> detail - refused && wantsReceiveOnly -> "receive-only, not forwarded" refused -> "login not verified" else -> detail } onReport( AprsReport( System.currentTimeMillis(), packetLine, ok, reported, - verified = !refused, receiveOnly = wantsReceiveOnly + verified = !refused ) ) if (ok) onState(AprsState.Connected) else onState(AprsState.Error) diff --git a/core/presentation/src/main/res/values-id/strings.xml b/core/presentation/src/main/res/values-id/strings.xml index 31bb11fb..810e8505 100644 --- a/core/presentation/src/main/res/values-id/strings.xml +++ b/core/presentation/src/main/res/values-id/strings.xml @@ -39,7 +39,6 @@ APRS: laporan terkirim APRS: laporan gagal - %1$s APRS-IS tidak memverifikasi passcode Anda. Laporan Anda tidak diteruskan - periksa tanda panggil dan passcode di Pengaturan. - APRS-IS menerima koneksi dalam mode terima-saja, laporan Anda tidak diteruskan. Masukkan passcode untuk mengirim. APRS belum dikonfigurasi - isi panggilan dulu Laporan terakhir: %1$s %2$s OK diff --git a/core/presentation/src/main/res/values-in/strings.xml b/core/presentation/src/main/res/values-in/strings.xml index c88f61a2..35b6749f 100644 --- a/core/presentation/src/main/res/values-in/strings.xml +++ b/core/presentation/src/main/res/values-in/strings.xml @@ -39,7 +39,6 @@ APRS: laporan terkirim APRS: laporan gagal - %1$s APRS-IS tidak memverifikasi passcode Anda. Laporan Anda tidak diteruskan - periksa tanda panggil dan passcode di Pengaturan. - APRS-IS menerima koneksi dalam mode terima-saja, laporan Anda tidak diteruskan. Masukkan passcode untuk mengirim. APRS belum dikonfigurasi - isi panggilan dulu Laporan terakhir: %1$s %2$s OK diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index 91e8695e..77b0862d 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -40,7 +40,6 @@ APRS: rapor gönderildi APRS: rapor başarısız - %1$s APRS-IS passcode bilginizi doğrulamadı. Raporlarınız iletilmiyor - Ayarlar bölümünde çağrı işareti ve passcode bilgisini kontrol edin. - APRS-IS baglantiyi yalnizca alma modunda kabul etti, raporlariniz iletilmiyor. Gondermek icin passcode girin. APRS yapılandırılmadı - önce çağrı girin Son rapor: %1$s %2$s OK diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index ce6ff4f9..c44f19e7 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -58,7 +58,6 @@ APRS: 上报成功 APRS: 上报失败 - %1$s APRS-IS 未验证你的 passcode, 上报不会被转发 - 请在设置里检查呼号与 passcode。 - APRS-IS 以只收模式接受了连接, 上报不会被转发。填入 passcode 才能发射。 APRS 未配置,请先填呼号 上次上报: %1$s %2$s 成功 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index 9802d5c3..879701e2 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -59,7 +59,6 @@ APRS: report sent OK APRS: report failed - %1$s APRS-IS did not verify your passcode. Your reports are not being passed on - check the callsign and passcode in Settings. - APRS-IS accepted the connection in receive-only mode, so your reports are not forwarded. Enter your passcode to transmit. APRS not configured - set callsign first Last report: %1$s %2$s OK diff --git a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt index ae799e9b..8f77c306 100644 --- a/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt +++ b/feature/cw/src/main/java/com/rtbishop/look4sat/feature/cw/CwDecodeScreen.kt @@ -51,8 +51,10 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import kotlinx.coroutines.launch +import androidx.compose.ui.draw.rotate +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -75,7 +77,6 @@ import kotlin.math.roundToInt * Not zero: an animated scroll settles a pixel or two short of the maximum, and an exact * comparison would drop out of follow-mode the moment it did. */ -private const val AUTOSCROLL_SLACK_PX = 4 /** * Full-page CW decoder backed by DeepCW. @@ -103,6 +104,10 @@ fun CwDecodeScreen() { } var permanentlyDenied by remember { mutableStateOf(false) } var isListening by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + // Hoisted so the toolbar button can reach it: the record below does not follow + // the decode, so jumping to the newest text has to be an explicit action. + val transcriptScroll = rememberScrollState() val decodedText by decoder.decodedText.collectAsState() val historyText by decoder.historyText.collectAsState() @@ -157,6 +162,17 @@ fun CwDecodeScreen() { .weight(1f) .padding(start = 12.dp) ) + // Jump to the newest text. The record below deliberately does not follow the decode, + // so this is how you get back to the bottom after reading earlier traffic. + IconButton(onClick = { scope.launch { transcriptScroll.animateScrollTo(transcriptScroll.maxValue) } }) { + Icon( + // The shared arrow points right; rotated to point down. Adding a second + // drawable for the same shape is how icon sets start to drift. + painter = painterResource(CoreR.drawable.ic_arrow), + contentDescription = stringResource(R.string.cw_scroll_newest), + modifier = Modifier.rotate(90f) + ) + } IconButton(onClick = { isListening = !isListening }) { Icon( painter = painterResource( @@ -232,34 +248,18 @@ fun CwDecodeScreen() { .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) ) { val transcript = (historyText + decodedText).ifEmpty { "…" } - val scroll = rememberScrollState() - // Follow the newest text, but stop as soon as the operator scrolls away, so - // reading back over earlier traffic is not undone by the next decode. + // This pane does not follow the decode. The single line above it is where new + // characters appear; this is the record, and a record that scrolls itself is worse + // than paper - you cannot read back over what just arrived because it keeps moving. // - // A boolean rather than comparing position against maxValue: maxValue is - // written during layout, after the composition that would read it, so such a - // comparison tests the previous frame's height and drifts short of the true - // bottom until it latches out of follow-mode altogether. - var following by remember { mutableStateOf(true) } - LaunchedEffect(scroll) { - snapshotFlow { scroll.isScrollInProgress to scroll.value } - .collect { (scrolling, value) -> - if (scrolling) following = value >= scroll.maxValue - AUTOSCROLL_SLACK_PX - } - } - LaunchedEffect(transcript, following) { - if (!following) return@LaunchedEffect - // Twice: the first pass lands at the height known when it started, the - // second covers growth that arrived while it was animating. - repeat(2) { - if (scroll.value < scroll.maxValue) scroll.animateScrollTo(scroll.maxValue) - } - } + // It used to auto-scroll to the bottom on every decode, dropping out of follow mode + // only once the operator had already fought it by scrolling away. Now it stays where + // it was put, and the button below jumps to the newest text on request. Text( text = transcript, modifier = Modifier .fillMaxSize() - .verticalScroll(scroll) + .verticalScroll(transcriptScroll) .padding(8.dp), fontSize = 16.sp, fontFamily = FontFamily.Monospace, diff --git a/feature/cw/src/main/res/values-id/app_values.xml b/feature/cw/src/main/res/values-id/app_values.xml index 3569a21a..d7092aea 100644 --- a/feature/cw/src/main/res/values-id/app_values.xml +++ b/feature/cw/src/main/res/values-id/app_values.xml @@ -3,6 +3,7 @@ Jeda pendekodean Lanjutkan pendekodean Hapus teks hasil dekode + Lompat ke teks terbaru Izin mikrofon diperlukan untuk pendekodean CW Berikan izin Buka pengaturan aplikasi diff --git a/feature/cw/src/main/res/values-in/app_values.xml b/feature/cw/src/main/res/values-in/app_values.xml index 3569a21a..d7092aea 100644 --- a/feature/cw/src/main/res/values-in/app_values.xml +++ b/feature/cw/src/main/res/values-in/app_values.xml @@ -3,6 +3,7 @@ Jeda pendekodean Lanjutkan pendekodean Hapus teks hasil dekode + Lompat ke teks terbaru Izin mikrofon diperlukan untuk pendekodean CW Berikan izin Buka pengaturan aplikasi diff --git a/feature/cw/src/main/res/values-tr/app_values.xml b/feature/cw/src/main/res/values-tr/app_values.xml index 52fe7962..6e66824a 100644 --- a/feature/cw/src/main/res/values-tr/app_values.xml +++ b/feature/cw/src/main/res/values-tr/app_values.xml @@ -3,6 +3,7 @@ Çözmeyi duraklat Çözmeyi sürdür Çözülen metni temizle + En yeni metne git CW çözümü için mikrofon izni gerekli İzin ver Uygulama ayarlarını aç diff --git a/feature/cw/src/main/res/values-zh/app_values.xml b/feature/cw/src/main/res/values-zh/app_values.xml index 42f7493f..a0e942ff 100644 --- a/feature/cw/src/main/res/values-zh/app_values.xml +++ b/feature/cw/src/main/res/values-zh/app_values.xml @@ -3,6 +3,7 @@ 暂停解码 继续解码 清空解码文本 + 跳到最新 CW 解码需要麦克风权限 授予权限 打开应用设置 diff --git a/feature/cw/src/main/res/values/app_values.xml b/feature/cw/src/main/res/values/app_values.xml index a7ec9789..2a3767da 100644 --- a/feature/cw/src/main/res/values/app_values.xml +++ b/feature/cw/src/main/res/values/app_values.xml @@ -3,6 +3,7 @@ Pause decoding Resume decoding Clear decoded text + Jump to the newest text Microphone permission is required for CW decoding Grant permission Open app settings