From b74cc1907fb346337d450b72798d29c19fb2777e Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Thu, 27 Aug 2026 19:20:16 -0400 Subject: [PATCH] Make a call that goes nowhere say so Android's Session.sendCallSignal called the send path and threw the result away. That result is the same Sent every other send returns - how many devices took it, which failed - so an offer that reached NOBODY was indistinguishable from one that rang. The caller watched "Calling..." for a signal that had gone nowhere, and neither device had anything to say so. The same shape as the message-loss bug fixed earlier today, on the one path where the UI never shows a delivery banner because a call is placed from inside a WebRTC callback. Also instruments the drops. An incoming offer can be discarded in five places in iOS's CallManager - already-ended, older than the dial timeout, busy, wrong state, unknown id - and every one was a bare `return`. Working out which of the five happened costs more than the logging does. Same treatment E2EInbox.dropped already gives ingest, and for the same reason. Not a fix for "Android calls iOS and nothing happens" - I have not reproduced that. It rules out and instruments: the wire shapes match, iOS builds its CallManager eagerly at sign-in so onCallSignal is always hooked, Android sets no from_addr so the federated fork is not swallowing it, and every clock (mac, host, relay, db) agrees within a second so the staleness check is not misfiring. The next attempt will say where it stopped instead of nothing. Co-Authored-By: Claude Opus 5 --- .../io/flamenet/messenger/app/CallManager.kt | 2 ++ .../messenger/app/MessengerViewModel.kt | 8 ++++++ .../io/flamenet/messenger/app/Session.kt | 27 ++++++++++++++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/CallManager.kt b/app/src/main/kotlin/io/flamenet/messenger/app/CallManager.kt index 19d32ad..92e3fa7 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/CallManager.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/CallManager.kt @@ -182,6 +182,8 @@ class CallManager( private var pendingOffer: Pair? = null fun onSignal(fromUser: Long, incoming: Content.CallSignal, turn: JSONObject?) { + android.util.Log.i("fnmsg-call", + "call signal in: ${incoming.action} from $fromUser state=$state") when (incoming.action) { Content.CallSignal.OFFER -> { // ⚠️ One call at a time. A second offer while busy is declined diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt index 6b93e85..50e85db 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt @@ -1026,6 +1026,14 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) { val current = session ?: return@launch val manager = callManager() val turn = withContext(Dispatchers.IO) { current.turnCredentials() } + // 🔴 **Whether the offer left, and to how many devices.** "I called + // them and nothing happened on the other phone" had no evidence on + // either side: the send path reports partial delivery to the UI as a + // banner, but a call places its offer from inside the WebRTC callback + // and that banner is never shown. A call to somebody with no registered + // device looked identical to a call that rang. + android.util.Log.i("fnmsg-call", + "placing call to $peerId, turn=${if (turn == null) "none" else "yes"}") update { it.copy(call = Phase.CallState( state = CallManager.State.DIALLING, diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/Session.kt b/app/src/main/kotlin/io/flamenet/messenger/app/Session.kt index c2b7c6a..301c4b2 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/Session.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/Session.kt @@ -397,9 +397,30 @@ class Session( * what you typed in it is not acceptable. The delivery counts still tell * the caller what happened. */ - /** Send one call signal to a peer. */ - fun sendCallSignal(peerUserId: Long, signal: io.flamenet.e2e.Content.CallSignal) { - conversations.sendCallSignal(peerUserId, signal) + /** + * Send one call signal to a peer. + * + * 🔴 **The result used to be discarded.** `sendCallSignal` returns the same + * `Sent` every other send does — how many devices took it, and which + * failed — and this threw it away. So an offer that reached NOBODY (the + * peer signed out, no registered device, a session that would not open) + * was indistinguishable from one that rang: the caller sat watching + * "Calling…" for a signal that had gone nowhere, and there was nothing on + * either device to say so. Exactly the shape of the message-loss bug on the + * text path, on the one path where the UI never shows a delivery banner. + * + * Returns it now, and logs the zero case, because a call is placed from + * inside a WebRTC callback where there is nowhere to put a banner. + */ + fun sendCallSignal(peerUserId: Long, signal: io.flamenet.e2e.Content.CallSignal): + Conversations.Sent { + val sent = conversations.sendCallSignal(peerUserId, signal) + if (!sent.reachedSomeone) { + android.util.Log.w("fnmsg-call", + "call signal ${signal.action} to $peerUserId reached NO devices: " + + sent.failed.entries.joinToString { "${it.key}=${it.value}" }) + } + return sent } /** TURN credentials, or null when this relay has none configured. */