diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 146b24526..01974a033 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -9067,6 +9067,76 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable { return declineCallNotificationIDReturnValue } } + //MARK: - subscribeToCallDeclineEvents + + var subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingCallsCount = 0 + var subscribeToCallDeclineEventsRtcNotificationEventIDListenerCallsCount: Int { + get { + if Thread.isMainThread { + return subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingCallsCount = newValue + } + } + } + } + var subscribeToCallDeclineEventsRtcNotificationEventIDListenerCalled: Bool { + return subscribeToCallDeclineEventsRtcNotificationEventIDListenerCallsCount > 0 + } + var subscribeToCallDeclineEventsRtcNotificationEventIDListenerReceivedArguments: (rtcNotificationEventID: String, listener: CallDeclineListener)? + var subscribeToCallDeclineEventsRtcNotificationEventIDListenerReceivedInvocations: [(rtcNotificationEventID: String, listener: CallDeclineListener)] = [] + + var subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingReturnValue: Result! + var subscribeToCallDeclineEventsRtcNotificationEventIDListenerReturnValue: Result! { + get { + if Thread.isMainThread { + return subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingReturnValue = newValue + } + } + } + } + var subscribeToCallDeclineEventsRtcNotificationEventIDListenerClosure: ((String, CallDeclineListener) -> Result)? + + func subscribeToCallDeclineEvents(rtcNotificationEventID: String, listener: CallDeclineListener) -> Result { + subscribeToCallDeclineEventsRtcNotificationEventIDListenerCallsCount += 1 + subscribeToCallDeclineEventsRtcNotificationEventIDListenerReceivedArguments = (rtcNotificationEventID: rtcNotificationEventID, listener: listener) + DispatchQueue.main.async { + self.subscribeToCallDeclineEventsRtcNotificationEventIDListenerReceivedInvocations.append((rtcNotificationEventID: rtcNotificationEventID, listener: listener)) + } + if let subscribeToCallDeclineEventsRtcNotificationEventIDListenerClosure = subscribeToCallDeclineEventsRtcNotificationEventIDListenerClosure { + return subscribeToCallDeclineEventsRtcNotificationEventIDListenerClosure(rtcNotificationEventID, listener) + } else { + return subscribeToCallDeclineEventsRtcNotificationEventIDListenerReturnValue + } + } //MARK: - matrixToPermalink var matrixToPermalinkUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Other/SDKListener.swift b/ElementX/Sources/Other/SDKListener.swift index 827476bdc..3569f4a20 100644 --- a/ElementX/Sources/Other/SDKListener.swift +++ b/ElementX/Sources/Other/SDKListener.swift @@ -98,6 +98,10 @@ extension SDKListener: RoomInfoListener where T == RoomInfo { func call(roomInfo: RoomInfo) { onUpdateClosure(roomInfo) } } +extension SDKListener: CallDeclineListener where T == String { + func call(declinerUserId: String) { onUpdateClosure(declinerUserId) } +} + // MARK: TimelineProxy extension SDKListener: PaginationStatusListener where T == RoomPaginationStatus { diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index ddaa6c63c..ef00e7c5a 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -9,6 +9,7 @@ import AVFoundation import CallKit import Combine import Foundation +import MatrixRustSDK import PushKit import UIKit @@ -41,14 +42,14 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe // 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() } + Task { await observeIncomingCall() } } } private var incomingCallRoomInfoCancellable: AnyCancellable? private var incomingCallID: CallID? { didSet { - Task { await observeIncomingCallRoomInfo() } + Task { await observeIncomingCall() } } } @@ -68,6 +69,8 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe actionsSubject.eraseToAnyPublisher() } + private var declineListenerHandle: TaskHandle? + override init() { pushRegistry = PKPushRegistry(queue: nil) @@ -305,7 +308,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe _ = await roomProxy.declineCall(notificationID: rtcNotificationID) } - private func observeIncomingCallRoomInfo() async { + private func observeIncomingCall() async { incomingCallRoomInfoCancellable = nil guard let incomingCallID else { @@ -341,17 +344,44 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe if !hasOngoingCall { MXLog.info("Call cancelled by remote") - - incomingCallRoomInfoCancellable = nil - endUnansweredCallTask?.cancel() - callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .remoteEnded) + reportEndedCall(incomingCallID: incomingCallID, reason: .remoteEnded) } else if participants.contains(roomProxy.ownUserID) { MXLog.info("Call answered elsewhere") - - incomingCallRoomInfoCancellable = nil - endUnansweredCallTask?.cancel() - callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .answeredElsewhere) + reportEndedCall(incomingCallID: incomingCallID, reason: .answeredElsewhere) } } + + guard let rtcNotificationID = incomingCallID.rtcNotificationID else { + MXLog.warning("Decline: No RTC notification ID found for the incoming call.") + return + } + + MXLog.info("Observe decline events for notification \(rtcNotificationID)") + + let listener: CallDeclineListener = SDKListener { [weak self] senderID in + guard let self else { return } + + MXLog.debug("Call declined event received from \(senderID)") + + if senderID == roomProxy.ownUserID { + // Stop ringing! + MXLog.debug("Call declined elsewhere") + reportEndedCall(incomingCallID: incomingCallID, reason: .declinedElsewhere) + } + } + + guard case let .success(handle) = roomProxy.subscribeToCallDeclineEvents(rtcNotificationEventID: rtcNotificationID, listener: listener) else { + MXLog.error("Unable to listen for decline events.") + return + } + + declineListenerHandle = handle + } + + private func reportEndedCall(incomingCallID: CallID, reason: CXCallEndedReason) { + declineListenerHandle?.cancel() + declineListenerHandle = nil + endUnansweredCallTask?.cancel() + callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: reason) } } diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index d6cbf2c45..a9e6b5efd 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -649,6 +649,17 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } } + /// Subscribe to call decline events from that rtc notification event. + func subscribeToCallDeclineEvents(rtcNotificationEventID: String, listener: CallDeclineListener) -> Result { + do { + let handle = try room.subscribeToCallDeclineEvents(rtcNotificationEventId: rtcNotificationEventID, listener: listener) + return .success(handle) + } catch { + MXLog.error("Failed observing rtc decline with error: \(error)") + return .failure(.sdkError(error)) + } + } + // MARK: - Permalinks func matrixToPermalink() async -> Result { diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index c0db8dad0..133c3cfb2 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -64,6 +64,13 @@ enum KnockRequestsState { case loaded([KnockRequestProxyProtocol]) } +struct RTCDeclinedEvent { + /// The sender of the decline event + let sender: String + /// The rtc.notification event that is beeing declined + let notificationEventID: String +} + // sourcery: AutoMockable protocol JoinedRoomProxyProtocol: RoomProxyProtocol { var infoPublisher: CurrentValuePublisher { get } @@ -170,6 +177,7 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol { func elementCallWidgetDriver(deviceID: String) -> ElementCallWidgetDriverProtocol func declineCall(notificationID: String) async -> Result + func subscribeToCallDeclineEvents(rtcNotificationEventID: String, listener: CallDeclineListener) -> Result // MARK: - Permalinks