diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt index b9d10539..7a5118ed 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorService.kt @@ -184,7 +184,15 @@ class MonitorService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // Every startForegroundService() re-arms the startForeground() obligation, // so every onStartCommand has to satisfy it again, no matter how it exits. - if (!promoteToForeground(lastNotification ?: MonitorNotifications.createEarlyNotification(this))) { + // + // Only reuse the cached notification while a monitor session is actually live. It is a fully + // built object whose `when` is frozen at build time, so re-promoting it to open a NEW session + // would re-post the previous session's last content under its original timestamp — which is + // how a stale "unknown device" frame survives a teardown/restart cycle. Rebuilding is not an + // option here: this has to stay cheap enough to satisfy the obligation before DI is ready. + val reusable = lastNotification?.takeIf { monitoringJob?.isActive == true } + if (reusable == null) lastNotification = null + if (!promoteToForeground(reusable ?: MonitorNotifications.createEarlyNotification(this))) { stopSelf(startId) return START_NOT_STICKY } diff --git a/app/src/test/java/eu/darken/capod/monitor/core/worker/MonitorServiceTest.kt b/app/src/test/java/eu/darken/capod/monitor/core/worker/MonitorServiceTest.kt index 1307bdf4..5790cfbd 100644 --- a/app/src/test/java/eu/darken/capod/monitor/core/worker/MonitorServiceTest.kt +++ b/app/src/test/java/eu/darken/capod/monitor/core/worker/MonitorServiceTest.kt @@ -125,6 +125,28 @@ class MonitorServiceTest { shadowOf(service).lastForegroundNotificationId shouldBe MonitorNotifications.NOTIFICATION_ID } + /** + * A built Notification's `when` is frozen, so re-promoting the previous session's last frame to + * open a new one re-posts stale content under its original timestamp — the way an "unknown + * device" placeholder survives a teardown/restart cycle. + */ + @Test + fun `a start with no live monitor does not reuse the previous notification`() { + val service = createService() + service.readyForMonitoring() + + val stale = notification("stale") + service.postPrimaryNotification(stale) + + // Monitor session has ended — the cached frame no longer reflects anything current. + service.setField("monitoringJob", null) + + service.onStartCommand(MonitorService.intent(context), 0, 1) shouldBe Service.START_STICKY + + shadowOf(service).lastForegroundNotification shouldNotBeSameInstanceAs stale + service.getField("lastNotification") shouldNotBeSameInstanceAs stale + } + /** * The FGS notification can outlive `stopSelf()`. Leaving it up strands whatever content was last * posted — including the unknown-device placeholder built before the first BLE scan batch landed.