fix: auto-reset CallManager state after call ends

CallManager now auto-resets from Ended to Idle after 2 seconds via
transitionToEnded(). Previously reset() was called from a
LaunchedEffect in CallScreen which could be cancelled when the
composable was disposed via popBack(), leaving the state stuck at
Ended and silently dropping subsequent incoming calls.

Also: CallController now observes CallManager state and auto-cleans
up the WebRTC session when a call ends (handles peer hangup case).

https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
This commit is contained in:
Claude
2026-04-02 00:06:35 +00:00
parent d263106562
commit f1fd5ec786
3 changed files with 36 additions and 14 deletions
@@ -48,6 +48,16 @@ class CallController(
private var currentCallId: String? = null
private var currentPeerPubKey: HexKey? = null
init {
scope.launch {
callManager.state.collect { state ->
if (state is CallState.Ended && webRtcSession != null) {
cleanup()
}
}
}
}
fun initiateCall(
peerPubKey: HexKey,
callType: CallType,
@@ -119,11 +119,6 @@ fun CallScreen(
}
is CallState.Ended -> {
LaunchedEffect(Unit) {
delay(2000)
callManager.reset()
onCallEnded()
}
CallInProgressUI(
peerPubKey = state.peerPubKey,
statusText = "Call ended",
@@ -55,9 +55,11 @@ class CallManager(
var onIceCandidateReceived: ((CallIceCandidateEvent) -> Unit)? = null
private var timeoutJob: Job? = null
private var resetJob: Job? = null
companion object {
const val CALL_TIMEOUT_MS = 60_000L // 60 seconds ringing timeout
const val ENDED_DISPLAY_MS = 2_000L // show "call ended" briefly before resetting
}
suspend fun initiateCall(
@@ -106,8 +108,7 @@ class CallManager(
if (current !is CallState.IncomingCall) return
val result = factory.createReject(current.callerPubKey, current.callId, signer = signer)
_state.value = CallState.Ended(current.callId, current.callerPubKey, EndReason.REJECTED)
cancelTimeout()
transitionToEnded(current.callId, current.callerPubKey, EndReason.REJECTED)
publishEvent(result.wrap)
}
@@ -126,8 +127,7 @@ class CallManager(
if (current !is CallState.Offering) return
if (event.callId() != current.callId) return
_state.value = CallState.Ended(current.callId, current.peerPubKey, EndReason.PEER_REJECTED)
cancelTimeout()
transitionToEnded(current.callId, current.peerPubKey, EndReason.PEER_REJECTED)
}
fun onIceCandidate(event: CallIceCandidateEvent) {
@@ -172,8 +172,7 @@ class CallManager(
}
val result = factory.createHangup(peerPubKey, callId, signer = signer)
_state.value = CallState.Ended(callId, peerPubKey, EndReason.HANGUP)
cancelTimeout()
transitionToEnded(callId, peerPubKey, EndReason.HANGUP)
publishEvent(result.wrap)
}
@@ -191,8 +190,7 @@ class CallManager(
if (callId != currentCallId) return
val peerPubKey = event.pubKey
_state.value = CallState.Ended(callId, peerPubKey, EndReason.PEER_HANGUP)
cancelTimeout()
transitionToEnded(callId, peerPubKey, EndReason.PEER_HANGUP)
}
fun onSignalingEvent(event: Event) {
@@ -229,6 +227,25 @@ class CallManager(
fun reset() {
_state.value = CallState.Idle
cancelTimeout()
resetJob?.cancel()
resetJob = null
}
private fun transitionToEnded(
callId: String,
peerPubKey: HexKey,
reason: EndReason,
) {
_state.value = CallState.Ended(callId, peerPubKey, reason)
cancelTimeout()
resetJob?.cancel()
resetJob =
scope.launch {
delay(ENDED_DISPLAY_MS)
if (_state.value is CallState.Ended) {
_state.value = CallState.Idle
}
}
}
private fun startTimeout(callId: String) {
@@ -250,7 +267,7 @@ class CallManager(
is CallState.IncomingCall -> current.callerPubKey
else -> return@launch
}
_state.value = CallState.Ended(callId, peerPubKey, EndReason.TIMEOUT)
transitionToEnded(callId, peerPubKey, EndReason.TIMEOUT)
}
}
}