From 0a67f74369f6b49221cee35f9e1cf1d764f269be Mon Sep 17 00:00:00 2001 From: QIU Date: Tue, 25 Aug 2026 15:18:15 +0000 Subject: [PATCH] fix(aprs): the service could not start at all on Android 10 and later The previous commit changed the manifest's foregroundServiceType to location and left startForeground passing FOREGROUND_SERVICE_TYPE_DATA_SYNC. AOSP requires the passed type to be a subset of the declared one - location is 0x08, dataSync is 0x01 - and throws IllegalArgumentException otherwise, a check that has been there since API 29. That throw landed in the surrounding catch, which calls stopSelf(). So APRS started, died, and said nothing. No notification, no beacon, no Toast, no last-report row, and the settings switch stayed on because the config had already been saved. This is worse than the defect the rewrite was written to fix: reporting success for packets that never left at least sometimes worked, whereas this never ran at all, on essentially every device in use, with no visible symptom. Two auditors found it independently by reading the constants against AOSP's own check. Two more findings from the same review. Receive-only was reported as a wrong passcode. Both a deliberate -1 and a mismatched entry log in with -1, and the server answers "unverified" to each, so the operator who chose receive-only - the one way to test a setup without putting anything on the network - was told to go and fix the passcode they had set on purpose. The report now carries whether receive-only was asked for, and says so instead. The card could show "failed - sent". The detail string was the write's own verdict, and a write that succeeds on a refused login is exactly the case where those two disagree. A failure now reports what actually failed. Also: the packet is built before connecting. The reporter used to open a session and log in only to discover it had nothing to send, which for an operator with no station position set meant a pointless login every five minutes. Still outstanding, and the reason this is not enough on its own: nothing tests the service, so neither this defect nor the missing line terminator in 7ac54f0a could have been caught by the suite. Both were found by audit. A location-typed foreground service on API 34+ may also require a granted location permission before startForeground, which the settings card does not request - that needs checking on hardware. --- .../look4sat/AprsForegroundService.kt | 13 +++- .../look4sat/core/data/aprs/AprsReporter.kt | 60 ++++++++++++++----- .../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 + 7 files changed, 62 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt index c9e67028..d9591d96 100644 --- a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt +++ b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt @@ -120,6 +120,11 @@ class AprsForegroundService : Service() { // problem is the passcode - and APRS-IS is dropping every packet meanwhile. 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) } @@ -147,7 +152,13 @@ class AprsForegroundService : Service() { try { val notif = buildNotification(cfg) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - startForeground(NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + // Must match the manifest attribute or the platform refuses the call: AOSP checks the + // passed type is a subset of the declared one, and location (0x08) does not contain + // dataSync (0x01). Changing the manifest without changing this line stopped the + // service dead on Android 10 and later - the IllegalArgumentException was caught + // below and turned into stopSelf(), so APRS did nothing and reported nothing while + // the settings switch stayed on. + startForeground(NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION) } else { startForeground(NOTIF_ID, notif) } 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 a8dfdb45..5a2cd0e7 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 @@ -43,7 +43,16 @@ data class AprsReport( * nothing reaching the network. A login whose response we simply could not parse leaves this * true, since the packets may be landing and the passcode is not at fault. */ - val verified: Boolean = true + val verified: Boolean = true, + /** + * True when the operator asked for a receive-only connection. + * + * Distinguished from a refused login because both log in with -1 and the server answers + * "unverified" to each: without this, deliberately choosing receive-only - the one way to + * 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 */ @@ -94,19 +103,9 @@ class AprsReporter( if (!cfg.enabled || cfg.callsign.isBlank()) return onState(AprsState.Connecting) try { - val c = client ?: AprsIsClient( - host = cfg.server, - port = cfg.port, - callsign = cfg.callsign, - ssid = cfg.ssid, - // Never derives one: a blank or wrong entry logs in receive-only rather than - // transmitting under a passcode the app invented for an unchecked licence. - passcode = AprsPasscode.loginValue(cfg.callsign, cfg.passcode), - version = "Look4Sat 4.5.4" - ).also { client = it } - if (!c.isConnected) c.connect() - onState(AprsState.Connected) - + // The packet is built BEFORE connecting: there is no reason to open a session and log + // in only to discover there is nothing to send, which happened every five minutes for + // an operator whose QTH was unset. val pos = positionProvider() val beacon = AprsBeacon.build( callsign = cfg.callsign, @@ -127,6 +126,23 @@ class AprsReporter( return } val packetLine = (beacon as AprsBeacon.Result.Line).text + + // Receive-only is a deliberate choice, not a mistake, and has to be carried through: + // it logs in with -1 exactly as a wrong passcode does, and the server answers + // "unverified" to both. + val wantsReceiveOnly = !AprsPasscode.canTransmit(cfg.callsign, cfg.passcode) + val c = client ?: AprsIsClient( + host = cfg.server, + port = cfg.port, + callsign = cfg.callsign, + ssid = cfg.ssid, + // Never derives one: a blank or wrong entry logs in receive-only rather than + // transmitting under a passcode the app invented for an unchecked licence. + passcode = AprsPasscode.loginValue(cfg.callsign, cfg.passcode), + version = "Look4Sat 4.5.4" + ).also { client = it } + if (!c.isConnected) c.connect() + onState(AprsState.Connected) val result = c.sendPacket(packetLine) val sent = result?.first == true val detail = result?.second ?: "no connection" @@ -137,7 +153,21 @@ class AprsReporter( // the operator to fix something that is not broken. val refused = c.isRefusedByServer val ok = sent && !refused - onReport(AprsReport(System.currentTimeMillis(), packetLine, ok, detail, !refused)) + // "sent" is the write's own verdict and reads as nonsense next to a failure - the card + // showed "failed - sent" for a refused login. When the refusal is what failed the + // 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 + ) + ) if (ok) onState(AprsState.Connected) else onState(AprsState.Error) } catch (e: Exception) { runCatching { client?.disconnect() } diff --git a/core/presentation/src/main/res/values-id/strings.xml b/core/presentation/src/main/res/values-id/strings.xml index ebaf0a91..0994afe7 100644 --- a/core/presentation/src/main/res/values-id/strings.xml +++ b/core/presentation/src/main/res/values-id/strings.xml @@ -39,6 +39,7 @@ 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 add9bf6b..e76ae75e 100644 --- a/core/presentation/src/main/res/values-in/strings.xml +++ b/core/presentation/src/main/res/values-in/strings.xml @@ -39,6 +39,7 @@ 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 b294c23d..cc3cf9ef 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -40,6 +40,7 @@ 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 4b794948..8093fcb6 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -41,6 +41,7 @@ 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 0199b181..8b9fd427 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -42,6 +42,7 @@ 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