diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index 9ca9b4a058..95e804d974 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -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() + private var incomingCallRoomInfoCancellable: AnyCancellable? private var incomingCallID: CallID? { didSet { - Task { - await observeIncomingCallRoomStateUpdates() - } + Task { await observeIncomingCallRoomInfo() } } } @@ -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 @@ -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 } @@ -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) } }