mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Clear incoming-call state on every path that ends a ringing call
Four end paths each cleaned up partially. Signed-off-by: Daniel Anderson <daniel.anderson@toptal.com>
This commit is contained in:
committed by
Stefan Ceriu
parent
9bfc97ad59
commit
4c8becc99e
@@ -115,7 +115,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: nil, isVoiceCall: false)
|
||||
}
|
||||
|
||||
incomingCallID = nil
|
||||
clearIncomingCallState()
|
||||
ongoingCallID = callID
|
||||
|
||||
// Don't bother starting another CallKit session as it won't work properly
|
||||
@@ -230,6 +230,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
|
||||
if let incomingCallID, incomingCallID.callKitID == callID.callKitID {
|
||||
callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .unanswered)
|
||||
clearIncomingCallState()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,6 +309,10 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
}
|
||||
}
|
||||
|
||||
if incomingCallID?.callKitID == action.callUUID {
|
||||
clearIncomingCallState()
|
||||
}
|
||||
|
||||
tearDownCallSession(sendEndCallAction: false)
|
||||
|
||||
action.fulfill()
|
||||
@@ -419,9 +424,20 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
}
|
||||
|
||||
private func reportEndedCall(incomingCallID: CallID, reason: CXCallEndedReason) {
|
||||
callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: reason)
|
||||
clearIncomingCallState()
|
||||
}
|
||||
|
||||
/// Cancels every subscription and task tied to a ringing incoming call and nils `incomingCallID`.
|
||||
/// Call this on every path that ends the incoming-call state (answered, declined, timed out,
|
||||
/// cancelled by remote, answered/declined elsewhere) so that follow-up calls to the same room
|
||||
/// start from a clean slate and the SDK decline-listener subscription doesn't leak.
|
||||
private func clearIncomingCallState() {
|
||||
endUnansweredCallTask?.cancel()
|
||||
endUnansweredCallTask = nil
|
||||
declineListenerHandle?.cancel()
|
||||
declineListenerHandle = nil
|
||||
endUnansweredCallTask?.cancel()
|
||||
callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: reason)
|
||||
incomingCallRoomInfoCancellable = nil
|
||||
incomingCallID = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +155,74 @@ final class ElementCallServiceTests {
|
||||
#expect(CallIntent.audio.rawValue == "audio")
|
||||
#expect(CallIntent.video.rawValue == "video")
|
||||
}
|
||||
|
||||
@Test
|
||||
func timeoutClearsIncomingCallStateBeforeNextPush() async {
|
||||
// Drive push #1 to its 60s unanswered timeout
|
||||
await confirmation { confirmation in
|
||||
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
|
||||
if reason == .unanswered {
|
||||
confirmation()
|
||||
}
|
||||
}
|
||||
|
||||
let firstPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 60)
|
||||
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: firstPayload, for: .voIP) { }
|
||||
|
||||
await testClock.advance(by: .seconds(70))
|
||||
}
|
||||
|
||||
callProvider.reportCallWithEndedAtReasonClosure = nil
|
||||
let firstCallUUID = callProvider.reportNewIncomingCallWithUpdateCompletionReceivedArguments?.uuid
|
||||
|
||||
// Send push #2 for the same room; the previous incoming state must be cleared,
|
||||
// so the second push gets a fresh CallID.
|
||||
await confirmation { confirmation in
|
||||
let secondPayload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 60)
|
||||
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: secondPayload, for: .voIP) {
|
||||
confirmation()
|
||||
}
|
||||
}
|
||||
|
||||
let secondCallUUID = callProvider.reportNewIncomingCallWithUpdateCompletionReceivedArguments?.uuid
|
||||
|
||||
#expect(firstCallUUID != nil)
|
||||
#expect(secondCallUUID != nil)
|
||||
#expect(firstCallUUID != secondCallUUID)
|
||||
|
||||
let unansweredCount = callProvider.reportCallWithEndedAtReasonReceivedInvocations.filter { $0.reason == .unanswered }.count
|
||||
#expect(unansweredCount == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func setupCallSessionCancelsPendingUnansweredTimeout() async {
|
||||
// Schedule the 60s unanswered timer via an incoming push
|
||||
await confirmation { confirmation in
|
||||
let payload = PKPushPayloadMock().updatingExpiration(currentDate, lifetime: 60)
|
||||
service.pushRegistry(pushRegistry, didReceiveIncomingPushWith: payload, for: .voIP) {
|
||||
confirmation()
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate the answer flow handing off to setupCallSession, which must cancel
|
||||
// the pending endUnansweredCallTask as part of clearing the incoming state.
|
||||
await service.setupCallSession(roomID: "!room:example.com", roomDisplayName: "welcome")
|
||||
|
||||
var unansweredFired = false
|
||||
callProvider.reportCallWithEndedAtReasonClosure = { _, _, reason in
|
||||
if reason == .unanswered {
|
||||
unansweredFired = true
|
||||
}
|
||||
}
|
||||
|
||||
// Advance past what would have been the 60s unanswered timeout
|
||||
await testClock.advance(by: .seconds(120))
|
||||
for _ in 0..<3 {
|
||||
await Task.yield()
|
||||
}
|
||||
|
||||
#expect(!unansweredFired, "endUnansweredCallTask should have been cancelled by setupCallSession")
|
||||
}
|
||||
}
|
||||
|
||||
private class PKPushPayloadMock: PKPushPayload {
|
||||
|
||||
Reference in New Issue
Block a user