From 7ea8698db900401aa0186ab3acf48e6a1dfe3bc4 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:56:10 +0300 Subject: [PATCH] Fix unbound CoroutineScope in NotificationReplyReceiver (#265) (#331) This commit addresses issue #265 where the NotificationReplyReceiver used an unbound CoroutineScope when handling asynchronous reply saves to the Room database. If the DAO operation hangs, the PendingResult.finish() method is never called, which could lead to an Application Not Responding (ANR) error. Changes made: - Wrapped the Room upsert operation inside a `withTimeout(5000L)` block within the IO coroutine in `NotificationReplyReceiver.kt`. - Ensures that if the operation exceeds 5 seconds, a TimeoutCancellationException is thrown and caught, allowing `pendingResult.finish()` to execute in the `finally` block. - Added a unit test in `NotificationReplyReceiverTest.kt` to verify that if the mock DAO hangs indefinitely, the PendingResult is still correctly finished after the timeout. Impact: - Prevents potential ANRs caused by stuck local DB transactions during inline notification replies. --- .../notification/NotificationReplyReceiver.kt | 34 ++++++++++--------- .../NotificationReplyReceiverTest.kt | 21 ++++++++++++ 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/notification/NotificationReplyReceiver.kt b/app/src/main/java/com/m57/hermescontrol/notification/NotificationReplyReceiver.kt index 12407c10..d7cac71b 100644 --- a/app/src/main/java/com/m57/hermescontrol/notification/NotificationReplyReceiver.kt +++ b/app/src/main/java/com/m57/hermescontrol/notification/NotificationReplyReceiver.kt @@ -53,22 +53,24 @@ open class NotificationReplyReceiver : BroadcastReceiver() { val pendingResult = goAsyncCompat() kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch { try { - val db = - com.m57.hermescontrol.data.local.HermesDatabase - .get(context) - val dao = db.chatMessageDao() - val entity = - com.m57.hermescontrol.data.local.ChatMessageEntity( - id = - java.util.UUID - .randomUUID() - .toString(), - sessionId = sessionId, - role = "USER", - content = replyText, - timestamp = System.currentTimeMillis(), - ) - dao.upsert(entity) + kotlinx.coroutines.withTimeout(5000L) { + val db = + com.m57.hermescontrol.data.local.HermesDatabase + .get(context) + val dao = db.chatMessageDao() + val entity = + com.m57.hermescontrol.data.local.ChatMessageEntity( + id = + java.util.UUID + .randomUUID() + .toString(), + sessionId = sessionId, + role = "USER", + content = replyText, + timestamp = System.currentTimeMillis(), + ) + dao.upsert(entity) + } } catch (e: Exception) { android.util.Log.e("NotificationReply", "Failed to save replied message to Room DB", e) } finally { diff --git a/app/src/test/java/com/m57/hermescontrol/notification/NotificationReplyReceiverTest.kt b/app/src/test/java/com/m57/hermescontrol/notification/NotificationReplyReceiverTest.kt index 4d197d45..8d17930b 100644 --- a/app/src/test/java/com/m57/hermescontrol/notification/NotificationReplyReceiverTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/notification/NotificationReplyReceiverTest.kt @@ -252,6 +252,27 @@ class NotificationReplyReceiverTest { verify { mockPendingResult.finish() } } + @Test + fun `Room hang triggers timeout and finishes pending result`() { + givenValidReply("session-abc", "Hello") + + // Make upsert hang indefinitely + coEvery { mockDao.upsert(any()) } coAnswers { + kotlinx.coroutines.delay(10000) + } + + receiver.onReceive(mockContext, mockIntent) + + // Wait longer than the 5-second timeout, but less than the 10-second delay + Thread.sleep(6000) + + // Timeout should be caught in catch block and logged + verify { android.util.Log.e("NotificationReply", any(), any()) } + + // Pending result must still finish after timeout + verify { mockPendingResult.finish() } + } + // ── Helpers ──────────────────────────────────────────────────────────── private fun givenValidReply(