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 43776c5bdc
commit 8ddec3a0d7
3 changed files with 36 additions and 14 deletions
@@ -48,6 +48,16 @@ class CallController(
private var currentCallId: String? = null private var currentCallId: String? = null
private var currentPeerPubKey: HexKey? = null private var currentPeerPubKey: HexKey? = null
init {
scope.launch {
callManager.state.collect { state ->
if (state is CallState.Ended && webRtcSession != null) {
cleanup()
}
}
}
}
fun initiateCall( fun initiateCall(
peerPubKey: HexKey, peerPubKey: HexKey,
callType: CallType, callType: CallType,
@@ -119,11 +119,6 @@ fun CallScreen(
} }
is CallState.Ended -> { is CallState.Ended -> {
LaunchedEffect(Unit) {
delay(2000)
callManager.reset()
onCallEnded()
}
CallInProgressUI( CallInProgressUI(
peerPubKey = state.peerPubKey, peerPubKey = state.peerPubKey,
statusText = "Call ended", statusText = "Call ended",
@@ -55,9 +55,11 @@ class CallManager(
var onIceCandidateReceived: ((CallIceCandidateEvent) -> Unit)? = null var onIceCandidateReceived: ((CallIceCandidateEvent) -> Unit)? = null
private var timeoutJob: Job? = null private var timeoutJob: Job? = null
private var resetJob: Job? = null
companion object { companion object {
const val CALL_TIMEOUT_MS = 60_000L // 60 seconds ringing timeout 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( suspend fun initiateCall(
@@ -106,8 +108,7 @@ class CallManager(
if (current !is CallState.IncomingCall) return if (current !is CallState.IncomingCall) return
val result = factory.createReject(current.callerPubKey, current.callId, signer = signer) val result = factory.createReject(current.callerPubKey, current.callId, signer = signer)
_state.value = CallState.Ended(current.callId, current.callerPubKey, EndReason.REJECTED) transitionToEnded(current.callId, current.callerPubKey, EndReason.REJECTED)
cancelTimeout()
publishEvent(result.wrap) publishEvent(result.wrap)
} }
@@ -126,8 +127,7 @@ class CallManager(
if (current !is CallState.Offering) return if (current !is CallState.Offering) return
if (event.callId() != current.callId) return if (event.callId() != current.callId) return
_state.value = CallState.Ended(current.callId, current.peerPubKey, EndReason.PEER_REJECTED) transitionToEnded(current.callId, current.peerPubKey, EndReason.PEER_REJECTED)
cancelTimeout()
} }
fun onIceCandidate(event: CallIceCandidateEvent) { fun onIceCandidate(event: CallIceCandidateEvent) {
@@ -172,8 +172,7 @@ class CallManager(
} }
val result = factory.createHangup(peerPubKey, callId, signer = signer) val result = factory.createHangup(peerPubKey, callId, signer = signer)
_state.value = CallState.Ended(callId, peerPubKey, EndReason.HANGUP) transitionToEnded(callId, peerPubKey, EndReason.HANGUP)
cancelTimeout()
publishEvent(result.wrap) publishEvent(result.wrap)
} }
@@ -191,8 +190,7 @@ class CallManager(
if (callId != currentCallId) return if (callId != currentCallId) return
val peerPubKey = event.pubKey val peerPubKey = event.pubKey
_state.value = CallState.Ended(callId, peerPubKey, EndReason.PEER_HANGUP) transitionToEnded(callId, peerPubKey, EndReason.PEER_HANGUP)
cancelTimeout()
} }
fun onSignalingEvent(event: Event) { fun onSignalingEvent(event: Event) {
@@ -229,6 +227,25 @@ class CallManager(
fun reset() { fun reset() {
_state.value = CallState.Idle _state.value = CallState.Idle
cancelTimeout() 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) { private fun startTimeout(callId: String) {
@@ -250,7 +267,7 @@ class CallManager(
is CallState.IncomingCall -> current.callerPubKey is CallState.IncomingCall -> current.callerPubKey
else -> return@launch else -> return@launch
} }
_state.value = CallState.Ended(callId, peerPubKey, EndReason.TIMEOUT) transitionToEnded(callId, peerPubKey, EndReason.TIMEOUT)
} }
} }
} }