Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix a couple of race conditions when observing room info updates for calls. #3487

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 30 additions & 21 deletions ElementX/Sources/Services/ElementCall/ElementCallService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,19 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
return CXProvider(configuration: configuration)
}()

private weak var clientProxy: ClientProxyProtocol?
private weak var clientProxy: ClientProxyProtocol? {
didSet {
// There's a race condition where a call starts when the app has been killed and the
// observation set in `incomingCallID` occurs *before* the user session is restored.
// So observe when the client proxy is set to fix this (the method guards for the call).
Task { await observeIncomingCallRoomInfo() }
}
}

private var cancellables = Set<AnyCancellable>()
private var incomingCallRoomInfoCancellable: AnyCancellable?
private var incomingCallID: CallID? {
didSet {
Task {
await observeIncomingCallRoomStateUpdates()
}
Task { await observeIncomingCallRoomInfo() }
}
}

Expand Down Expand Up @@ -260,7 +265,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe

// MARK: - Private

func tearDownCallSession(sendEndCallAction: Bool = true) {
private func tearDownCallSession(sendEndCallAction: Bool = true) {
if sendEndCallAction, let ongoingCallID {
let transaction = CXTransaction(action: CXEndCallAction(call: ongoingCallID.callKitID))
callController.request(transaction) { error in
Expand All @@ -273,30 +278,35 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
ongoingCallID = nil
}

func observeIncomingCallRoomStateUpdates() async {
cancellables.removeAll()
private func observeIncomingCallRoomInfo() async {
incomingCallRoomInfoCancellable = nil

guard let clientProxy, let incomingCallID else {
guard let incomingCallID else {
MXLog.info("No incoming call to observe for.")
return
}

guard let clientProxy else {
MXLog.warning("A ClientProxy is needed to fetch the room.")
return
}

guard case let .joined(roomProxy) = await clientProxy.roomForIdentifier(incomingCallID.roomID) else {
MXLog.warning("Failed to fetch a joined room for the incoming call.")
return
}

roomProxy.subscribeToRoomInfoUpdates()

// There's no incoming event for call cancellations so try to infer
// it from what we have. If the call is running before subscribing then wait
// for it to change to `false` otherwise wait for it to turn `true` before
// changing to `false`
let isCallOngoing = roomProxy.infoPublisher.value.hasRoomCall

roomProxy
incomingCallRoomInfoCancellable = roomProxy
.infoPublisher
.compactMap { ($0.hasRoomCall, $0.activeRoomCallParticipants) }
.removeDuplicates { $0 == $1 }
.dropFirst(isCallOngoing ? 0 : 1)
.drop(while: { hasRoomCall, _ in
// Filter all updates before hasRoomCall becomes `true`. Then we can correctly
// detect its change to `false` to stop ringing when the caller hangs up.
!hasRoomCall
})
.sink { [weak self] hasOngoingCall, activeRoomCallParticipants in
guard let self else { return }

Expand All @@ -305,17 +315,16 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
if !hasOngoingCall {
MXLog.info("Call cancelled by remote")

cancellables.removeAll()
incomingCallRoomInfoCancellable = nil
endUnansweredCallTask?.cancel()
callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .remoteEnded)
} else if participants.contains(roomProxy.ownUserID) {
MXLog.info("Call anwered elsewhere")
MXLog.info("Call answered elsewhere")

cancellables.removeAll()
incomingCallRoomInfoCancellable = nil
endUnansweredCallTask?.cancel()
callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .answeredElsewhere)
}
}
.store(in: &cancellables)
}
}
Loading