diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 34f27b711..358057a4b 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -8997,6 +8997,76 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable { return elementCallWidgetDriverDeviceIDReturnValue } } + //MARK: - declineCall + + var declineCallNotificationIDUnderlyingCallsCount = 0 + var declineCallNotificationIDCallsCount: Int { + get { + if Thread.isMainThread { + return declineCallNotificationIDUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = declineCallNotificationIDUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + declineCallNotificationIDUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + declineCallNotificationIDUnderlyingCallsCount = newValue + } + } + } + } + var declineCallNotificationIDCalled: Bool { + return declineCallNotificationIDCallsCount > 0 + } + var declineCallNotificationIDReceivedNotificationID: String? + var declineCallNotificationIDReceivedInvocations: [String] = [] + + var declineCallNotificationIDUnderlyingReturnValue: Result! + var declineCallNotificationIDReturnValue: Result! { + get { + if Thread.isMainThread { + return declineCallNotificationIDUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = declineCallNotificationIDUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + declineCallNotificationIDUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + declineCallNotificationIDUnderlyingReturnValue = newValue + } + } + } + } + var declineCallNotificationIDClosure: ((String) async -> Result)? + + func declineCall(notificationID: String) async -> Result { + declineCallNotificationIDCallsCount += 1 + declineCallNotificationIDReceivedNotificationID = notificationID + DispatchQueue.main.async { + self.declineCallNotificationIDReceivedInvocations.append(notificationID) + } + if let declineCallNotificationIDClosure = declineCallNotificationIDClosure { + return await declineCallNotificationIDClosure(notificationID) + } else { + return declineCallNotificationIDReturnValue + } + } //MARK: - matrixToPermalink var matrixToPermalinkUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index 66a98b127..ddaa6c63c 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -16,6 +16,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe private struct CallID: Equatable { let callKitID: UUID let roomID: String + let rtcNotificationID: String? } private let pushRegistry: PKPushRegistry @@ -93,7 +94,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe let callID = if let incomingCallID, incomingCallID.roomID == roomID { incomingCallID } else { - CallID(callKitID: UUID(), roomID: roomID) + CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: nil) } incomingCallID = nil @@ -146,12 +147,17 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe return } + guard let rtcNotificationID = payload.dictionaryPayload[ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue] as? String else { + MXLog.error("Something went wrong, missing rtc notification event identifier for incoming voip call: \(payload)") + return + } + guard ongoingCallID?.roomID != roomID else { MXLog.warning("Call already ongoing for room \(roomID), ignoring incoming push") return } - let callID = CallID(callKitID: UUID(), roomID: roomID) + let callID = CallID(callKitID: UUID(), roomID: roomID, rtcNotificationID: rtcNotificationID) incomingCallID = callID let roomDisplayName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String @@ -253,6 +259,12 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe actionsSubject.send(.endCall(roomID: ongoingCallID.roomID)) } + if let incomingCallID { + Task { + await sendDeclineCallEvent(incomingCallID) + } + } + tearDownCallSession(sendEndCallAction: false) action.fulfill() @@ -274,6 +286,25 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe ongoingCallID = nil } + private func sendDeclineCallEvent(_ incomingCallID: CallID) async { + guard let rtcNotificationID = incomingCallID.rtcNotificationID else { + MXLog.info("No rtc notification event to decline.") + 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 + } + + _ = await roomProxy.declineCall(notificationID: rtcNotificationID) + } + private func observeIncomingCallRoomInfo() async { incomingCallRoomInfoCancellable = nil diff --git a/ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift b/ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift index 35ceeb720..34e2f8b36 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift @@ -10,6 +10,9 @@ import Foundation enum ElementCallServiceNotificationKey: String { case roomID case roomDisplayName + /// When an incoming call is set to ring, there will be a `m.rtc.notification`event (MSC4075). + /// Keep the notification event id as it is needed to decline calls (MSC4310). + case rtcNotifyEventID } let ElementCallServiceNotificationDiscardDelta = 15.0 diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index a13d26ac1..d6cbf2c45 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -639,6 +639,16 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { ElementCallWidgetDriver(room: room, deviceID: deviceID) } + func declineCall(notificationID: String) async -> Result { + do { + try await room.declineCall(rtcNotificationEventId: notificationID) + return .success(()) + } catch { + MXLog.error("Failed to decline rtc notification \(notificationID) 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 a9b28c239..c0db8dad0 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -169,6 +169,7 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol { // MARK: - Element Call func elementCallWidgetDriver(deviceID: String) -> ElementCallWidgetDriverProtocol + func declineCall(notificationID: String) async -> Result // MARK: - Permalinks diff --git a/NSE/Sources/NotificationHandler.swift b/NSE/Sources/NotificationHandler.swift index 43d2b8795..edb0845b9 100644 --- a/NSE/Sources/NotificationHandler.swift +++ b/NSE/Sources/NotificationHandler.swift @@ -126,6 +126,7 @@ class NotificationHandler { return .processedShouldDiscard case .callNotify(let notifyType): return await handleCallNotification(notifyType: notifyType, + rtcNotifyEventID: event.eventId(), timestamp: event.timestamp(), roomID: itemProxy.roomID, roomDisplayName: itemProxy.roomDisplayName) @@ -153,6 +154,7 @@ class NotificationHandler { /// Handle incoming call notifications. /// - Returns: A boolean indicating whether the notification was handled and should now be discarded. private func handleCallNotification(notifyType: NotifyType, + rtcNotifyEventID: String, timestamp: Timestamp, roomID: String, roomDisplayName: String) async -> NotificationProcessingResult { @@ -206,7 +208,8 @@ class NotificationHandler { } let payload = [ElementCallServiceNotificationKey.roomID.rawValue: roomID, - ElementCallServiceNotificationKey.roomDisplayName.rawValue: roomDisplayName] + ElementCallServiceNotificationKey.roomDisplayName.rawValue: roomDisplayName, + ElementCallServiceNotificationKey.rtcNotifyEventID.rawValue: rtcNotifyEventID] do { try await CXProvider.reportNewIncomingVoIPPushPayload(payload)