From 7d7abeb31e532c738348eefda325c29c703c6d5b Mon Sep 17 00:00:00 2001 From: QIU Date: Fri, 14 Aug 2026 15:38:08 +0000 Subject: [PATCH] fix(aprs): prevent duplicate reporters on repeated service starts Every ACTION_START - and every null intent delivered by START_STICKY - called startReporting(), which always constructed a new AprsReporter and overwrote the field without stopping the old one. AprsReporter owns an independent SupervisorJob + periodic while(isActive) loop, so every overwritten instance kept reporting forever and could no longer be reached by ACTION_STOP. Simulation: five ACTION_START events: 5 running reporters, 4 leaked -> fixed: 1 / 0 START + 3 sticky restarts: 4 running, 3 leaked -> fixed: 1 / 0 mixed real sequence: 4 running, 3 leaked -> fixed: 1 / 0 At the default 10-minute interval, four leaked reporters send 24 duplicate position packets per hour and open 24 needless connections; this also amplifies the connect-time socket leak fixed earlier. startReporting now returns when the current reporter is active. Config changes remain correct: AprsCard explicitly sends ACTION_STOP before ACTION_START, so the old reporter is stopped and nulled before the new configuration starts. :app:compileReleaseKotlin BUILD SUCCESSFUL. --- .../com/rtbishop/look4sat/AprsForegroundService.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt index 7ff099de..03dc41ca 100644 --- a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt +++ b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt @@ -64,6 +64,16 @@ class AprsForegroundService : Service() { } private fun startReporting() { + // onStartCommand reaches here for every ACTION_START and for the null + // intent that START_STICKY delivers on restart. Without this guard each + // call built a fresh AprsReporter and overwrote the field, leaving the + // previous one running with its own scope and timer: the server then + // received one duplicate position report per leaked instance per cycle, + // and ACTION_STOP could only ever stop the newest one. + reporter?.let { existing -> + if (existing.isRunning) return + existing.stop() + } val cfg = AprsStore.loadConfig(this) if (!cfg.enabled || cfg.callsign.isBlank()) { runCatching {