fix(#267): prevent ghost notifications on app foreground transition (#328)

- Replace volatile boolean with AtomicBoolean for thread-safe state access
- Add a 500ms debounce to notification logic to account for delayed foreground transitions
- Update ChatNotificationService unit tests to reflect the new AtomicBoolean state type

This fixes a race condition where a message arriving just as the user opens the app would incorrectly trigger a foreground notification.
This commit is contained in:
M57 authored and GitHub committed 2026-06-23 12:08:03 +03:00
1 parent 586f463c9c
commit ecbea6d2b9
2 files changed
+41 -39

No files matched your search

@@ -21,8 +21,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import java.util.concurrent.atomic.AtomicBoolean
/**
* Foreground service that keeps the WebSocket connection alive while the app
@@ -57,11 +59,10 @@ class ChatNotificationService : Service() {
internal const val NOTIFICATION_ID = 1
internal const val PENDING_NOTIFICATION_ID = 2
@Volatile
private var isAppInForeground = false
private val isAppInForeground = AtomicBoolean(false)
fun setAppForeground(foreground: Boolean) {
isAppInForeground = foreground
isAppInForeground.set(foreground)
}
}
@@ -79,22 +80,27 @@ class ChatNotificationService : Service() {
eventCollector =
serviceScope.launch {
HermesWsClient.events.collect { event ->
if (!isAppInForeground) {
when (event) {
is WsEvent.MessageComplete -> {
val preview =
event.text
.take(100)
.replace("\n", " ")
.ifBlank { getString(R.string.notif_new_message) }
showReplyNotification(preview, event.sessionId)
}
if (!isAppInForeground.get()) {
launch {
delay(500)
if (!isAppInForeground.get()) {
when (event) {
is WsEvent.MessageComplete -> {
val preview =
event.text
.take(100)
.replace("\n", " ")
.ifBlank { getString(R.string.notif_new_message) }
showReplyNotification(preview, event.sessionId)
}
is WsEvent.ClarifyRequest -> {
showReplyNotification(getString(R.string.notif_clarification_needed), null)
}
is WsEvent.ClarifyRequest -> {
showReplyNotification(getString(R.string.notif_clarification_needed), null)
}
else -> {}
else -> {}
}
}
}
}
}