Listen to call decline to stop ringing when declined from other device (#4505)

* Listen to call decline to stop ringing when declined from other device

* use proper swift naming convention for Id/ID

* fix Force unwrapping

* fix lint

* An approach without the custom publisher.

* review: correct naming convention

* review: revert some invisible char/tab changes

* add ref to the room proxy in the closure

---------

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Valere Fedronic
2025-09-18 16:11:03 +02:00
committed by GitHub
parent af2c13c7dd
commit cb62112159
5 changed files with 134 additions and 11 deletions

View File

@@ -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<TaskHandle, RoomProxyError>!
var subscribeToCallDeclineEventsRtcNotificationEventIDListenerReturnValue: Result<TaskHandle, RoomProxyError>! {
get {
if Thread.isMainThread {
return subscribeToCallDeclineEventsRtcNotificationEventIDListenerUnderlyingReturnValue
} else {
var returnValue: Result<TaskHandle, RoomProxyError>? = 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<TaskHandle, RoomProxyError>)?
func subscribeToCallDeclineEvents(rtcNotificationEventID: String, listener: CallDeclineListener) -> Result<TaskHandle, RoomProxyError> {
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

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -649,6 +649,17 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
}
}
/// Subscribe to call decline events from that rtc notification event.
func subscribeToCallDeclineEvents(rtcNotificationEventID: String, listener: CallDeclineListener) -> Result<TaskHandle, RoomProxyError> {
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<URL, RoomProxyError> {

View File

@@ -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<RoomInfoProxyProtocol, Never> { get }
@@ -170,6 +177,7 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
func elementCallWidgetDriver(deviceID: String) -> ElementCallWidgetDriverProtocol
func declineCall(notificationID: String) async -> Result<Void, RoomProxyError>
func subscribeToCallDeclineEvents(rtcNotificationEventID: String, listener: CallDeclineListener) -> Result<TaskHandle, RoomProxyError>
// MARK: - Permalinks