Skip to content

Commit 1c8addd

Browse files
committed
Listen to call decline to stop ringing when declined from other device
1 parent 8b30327 commit 1c8addd

File tree

5 files changed

+261
-3
lines changed

5 files changed

+261
-3
lines changed

ElementX/Sources/Mocks/Generated/GeneratedMocks.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8997,6 +8997,76 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable {
89978997
return declineCallNotificationIdReturnValue
89988998
}
89998999
}
9000+
//MARK: - callDeclineEventPublisher
9001+
9002+
var callDeclineEventPublisherNotificationIdUnderlyingCallsCount = 0
9003+
var callDeclineEventPublisherNotificationIdCallsCount: Int {
9004+
get {
9005+
if Thread.isMainThread {
9006+
return callDeclineEventPublisherNotificationIdUnderlyingCallsCount
9007+
} else {
9008+
var returnValue: Int? = nil
9009+
DispatchQueue.main.sync {
9010+
returnValue = callDeclineEventPublisherNotificationIdUnderlyingCallsCount
9011+
}
9012+
9013+
return returnValue!
9014+
}
9015+
}
9016+
set {
9017+
if Thread.isMainThread {
9018+
callDeclineEventPublisherNotificationIdUnderlyingCallsCount = newValue
9019+
} else {
9020+
DispatchQueue.main.sync {
9021+
callDeclineEventPublisherNotificationIdUnderlyingCallsCount = newValue
9022+
}
9023+
}
9024+
}
9025+
}
9026+
var callDeclineEventPublisherNotificationIdCalled: Bool {
9027+
return callDeclineEventPublisherNotificationIdCallsCount > 0
9028+
}
9029+
var callDeclineEventPublisherNotificationIdReceivedRtcNotificationEventId: String?
9030+
var callDeclineEventPublisherNotificationIdReceivedInvocations: [String] = []
9031+
9032+
var callDeclineEventPublisherNotificationIdUnderlyingReturnValue: AnyPublisher<RtcDeclinedEvent, Never>!
9033+
var callDeclineEventPublisherNotificationIdReturnValue: AnyPublisher<RtcDeclinedEvent, Never>! {
9034+
get {
9035+
if Thread.isMainThread {
9036+
return callDeclineEventPublisherNotificationIdUnderlyingReturnValue
9037+
} else {
9038+
var returnValue: AnyPublisher<RtcDeclinedEvent, Never>? = nil
9039+
DispatchQueue.main.sync {
9040+
returnValue = callDeclineEventPublisherNotificationIdUnderlyingReturnValue
9041+
}
9042+
9043+
return returnValue!
9044+
}
9045+
}
9046+
set {
9047+
if Thread.isMainThread {
9048+
callDeclineEventPublisherNotificationIdUnderlyingReturnValue = newValue
9049+
} else {
9050+
DispatchQueue.main.sync {
9051+
callDeclineEventPublisherNotificationIdUnderlyingReturnValue = newValue
9052+
}
9053+
}
9054+
}
9055+
}
9056+
var callDeclineEventPublisherNotificationIdClosure: ((String) -> AnyPublisher<RtcDeclinedEvent, Never>)?
9057+
9058+
func callDeclineEventPublisher(notificationId rtcNotificationEventId: String) -> AnyPublisher<RtcDeclinedEvent, Never> {
9059+
callDeclineEventPublisherNotificationIdCallsCount += 1
9060+
callDeclineEventPublisherNotificationIdReceivedRtcNotificationEventId = rtcNotificationEventId
9061+
DispatchQueue.main.async {
9062+
self.callDeclineEventPublisherNotificationIdReceivedInvocations.append(rtcNotificationEventId)
9063+
}
9064+
if let callDeclineEventPublisherNotificationIdClosure = callDeclineEventPublisherNotificationIdClosure {
9065+
return callDeclineEventPublisherNotificationIdClosure(rtcNotificationEventId)
9066+
} else {
9067+
return callDeclineEventPublisherNotificationIdReturnValue
9068+
}
9069+
}
90009070
//MARK: - matrixToPermalink
90019071

90029072
var matrixToPermalinkUnderlyingCallsCount = 0

ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14867,6 +14867,71 @@ open class RoomSDKMock: MatrixRustSDK.Room, @unchecked Sendable {
1486714867
}
1486814868
}
1486914869

14870+
//MARK: - newLatestEvent
14871+
14872+
var newLatestEventUnderlyingCallsCount = 0
14873+
open var newLatestEventCallsCount: Int {
14874+
get {
14875+
if Thread.isMainThread {
14876+
return newLatestEventUnderlyingCallsCount
14877+
} else {
14878+
var returnValue: Int? = nil
14879+
DispatchQueue.main.sync {
14880+
returnValue = newLatestEventUnderlyingCallsCount
14881+
}
14882+
14883+
return returnValue!
14884+
}
14885+
}
14886+
set {
14887+
if Thread.isMainThread {
14888+
newLatestEventUnderlyingCallsCount = newValue
14889+
} else {
14890+
DispatchQueue.main.sync {
14891+
newLatestEventUnderlyingCallsCount = newValue
14892+
}
14893+
}
14894+
}
14895+
}
14896+
open var newLatestEventCalled: Bool {
14897+
return newLatestEventCallsCount > 0
14898+
}
14899+
14900+
var newLatestEventUnderlyingReturnValue: LatestEventValue!
14901+
open var newLatestEventReturnValue: LatestEventValue! {
14902+
get {
14903+
if Thread.isMainThread {
14904+
return newLatestEventUnderlyingReturnValue
14905+
} else {
14906+
var returnValue: LatestEventValue? = nil
14907+
DispatchQueue.main.sync {
14908+
returnValue = newLatestEventUnderlyingReturnValue
14909+
}
14910+
14911+
return returnValue!
14912+
}
14913+
}
14914+
set {
14915+
if Thread.isMainThread {
14916+
newLatestEventUnderlyingReturnValue = newValue
14917+
} else {
14918+
DispatchQueue.main.sync {
14919+
newLatestEventUnderlyingReturnValue = newValue
14920+
}
14921+
}
14922+
}
14923+
}
14924+
open var newLatestEventClosure: (() async -> LatestEventValue)?
14925+
14926+
open override func newLatestEvent() async -> LatestEventValue {
14927+
newLatestEventCallsCount += 1
14928+
if let newLatestEventClosure = newLatestEventClosure {
14929+
return await newLatestEventClosure()
14930+
} else {
14931+
return newLatestEventReturnValue
14932+
}
14933+
}
14934+
1487014935
//MARK: - ownUserId
1487114936

1487214937
var ownUserIdUnderlyingCallsCount = 0

ElementX/Sources/Services/ElementCall/ElementCallService.swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import AVFoundation
99
import CallKit
1010
import Combine
1111
import Foundation
12+
import MatrixRustSDK
1213
import PushKit
1314
import UIKit
1415

@@ -48,7 +49,10 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
4849
private var incomingCallRoomInfoCancellable: AnyCancellable?
4950
private var incomingCallID: CallID? {
5051
didSet {
51-
Task { await observeIncomingCallRoomInfo() }
52+
Task {
53+
await observeIncomingCallRoomInfo()
54+
await observeDeclineEvents()
55+
}
5256
}
5357
}
5458

@@ -68,6 +72,8 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
6872
actionsSubject.eraseToAnyPublisher()
6973
}
7074

75+
private var declineListenerCancellable: AnyCancellable?
76+
7177
override init() {
7278
pushRegistry = PKPushRegistry(queue: nil)
7379

@@ -354,4 +360,43 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
354360
}
355361
}
356362
}
363+
364+
private func observeDeclineEvents() async {
365+
guard let incomingCallID else {
366+
MXLog.info("Decline: No incoming call to observe for.")
367+
return
368+
}
369+
370+
guard let clientProxy else {
371+
MXLog.warning("Decline: A ClientProxy is needed to fetch the room.")
372+
return
373+
}
374+
375+
guard let rtcNotificationId = incomingCallID.rtcNotificationId else {
376+
MXLog.warning("Decline: No RTC notification ID found for the incoming call.")
377+
return
378+
}
379+
380+
guard case let .joined(roomProxy) = await clientProxy.roomForIdentifier(incomingCallID.roomID) else {
381+
MXLog.warning("Failed to fetch a joined room for the incoming call.")
382+
return
383+
}
384+
let ownUserId = clientProxy.userID
385+
386+
MXLog.info("Observe decline events for notification \(rtcNotificationId)")
387+
388+
declineListenerCancellable = roomProxy
389+
.callDeclineEventPublisher(notificationId: rtcNotificationId)
390+
.sink { ev in
391+
MXLog.debug("Call declined event received from \(ev.sender)")
392+
if ev.sender == ownUserId {
393+
// Stop ringing!
394+
MXLog.debug("Call declined elsewhere")
395+
self.declineListenerCancellable?.cancel()
396+
self.declineListenerCancellable = nil
397+
self.endUnansweredCallTask?.cancel()
398+
self.callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .declinedElsewhere)
399+
}
400+
}
401+
}
357402
}

ElementX/Sources/Services/Room/JoinedRoomProxy.swift

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
7070
var knockRequestsStatePublisher: CurrentValuePublisher<KnockRequestsState, Never> {
7171
knockRequestsStateSubject.asCurrentValuePublisher()
7272
}
73-
73+
7474
init(roomListService: RoomListServiceProtocol,
7575
room: RoomProtocol,
7676
appSettings: AppSettings) async throws {
@@ -649,6 +649,22 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
649649
}
650650
}
651651

652+
/// Subscribe to call decline events from that rtc notification event.
653+
func callDeclineEventPublisher(notificationId rtcNotificationEventId: String) -> AnyPublisher<RtcDeclinedEvent, Never> {
654+
let publisher = DeclineCallbackPublisher(room: self, eventId: rtcNotificationEventId)
655+
return publisher.eraseToAnyPublisher()
656+
}
657+
658+
func subscribeToCallDeclineEvents(rtcNotificationEventId: String, listener: RoomCallDeclineListener) -> Result<TaskHandle, RoomProxyError> {
659+
do {
660+
let handle = try room.subscribeToCallDeclineEvents(rtcNotificationEventId: rtcNotificationEventId, listener: listener)
661+
return .success(handle)
662+
} catch {
663+
MXLog.error("Failed observing rtc decline with error: \(error)")
664+
return .failure(.sdkError(error))
665+
}
666+
}
667+
652668
// MARK: - Permalinks
653669

654670
func matrixToPermalink() async -> Result<URL, RoomProxyError> {
@@ -815,3 +831,57 @@ private final class RoomKnockRequestsListener: KnockRequestsListener {
815831
onUpdateClosure(joinRequests)
816832
}
817833
}
834+
835+
final class RoomCallDeclineListener: CallDeclineListener {
836+
private let onUpdateClosure: (RtcDeclinedEvent) -> Void
837+
private let notificationId: String
838+
839+
init(notificationId: String, onUpdateClosure: @escaping (RtcDeclinedEvent) -> Void) {
840+
self.notificationId = notificationId
841+
self.onUpdateClosure = onUpdateClosure
842+
}
843+
844+
func call(declinerUserId: String) {
845+
onUpdateClosure(.init(sender: declinerUserId, notificationEventId: notificationId))
846+
}
847+
}
848+
849+
// Helper to transform callback to publisher while correctly retaining the task handle and listener.
850+
struct DeclineCallbackPublisher: Publisher {
851+
typealias Output = RtcDeclinedEvent
852+
typealias Failure = Never
853+
854+
let room: JoinedRoomProxy
855+
let eventId: String
856+
857+
func receive<S>(subscriber: S) where S: Subscriber, Never == S.Failure, RtcDeclinedEvent == S.Input {
858+
let subscription = Inner(subscriber: subscriber, room: room, eventId: eventId)
859+
subscriber.receive(subscription: subscription)
860+
}
861+
862+
private final class Inner<S: Subscriber>: Subscription
863+
where S.Input == RtcDeclinedEvent, S.Failure == Never {
864+
private var subscriber: S?
865+
private var handle: TaskHandle?
866+
private var listener: RoomCallDeclineListener?
867+
868+
init(subscriber: S, room: JoinedRoomProxy, eventId: String) {
869+
self.subscriber = subscriber
870+
listener = RoomCallDeclineListener(notificationId: eventId) { [weak self] ev in
871+
_ = self?.subscriber?.receive(ev)
872+
}
873+
handle = try? room.subscribeToCallDeclineEvents(rtcNotificationEventId: eventId, listener: listener!).get()
874+
}
875+
876+
func request(_ demand: Subscribers.Demand) {
877+
// nop
878+
}
879+
880+
func cancel() {
881+
handle?.cancel()
882+
handle = nil
883+
listener = nil
884+
subscriber = nil
885+
}
886+
}
887+
}

ElementX/Sources/Services/Room/RoomProxyProtocol.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ enum KnockRequestsState {
6464
case loaded([KnockRequestProxyProtocol])
6565
}
6666

67+
struct RtcDeclinedEvent {
68+
/// The sender of the decline event
69+
let sender: String
70+
/// The rtc.notification event that is beeing declined
71+
let notificationEventId: String
72+
}
73+
6774
// sourcery: AutoMockable
6875
protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
6976
var infoPublisher: CurrentValuePublisher<RoomInfoProxyProtocol, Never> { get }
@@ -75,7 +82,7 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
7582
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { get }
7683

7784
var knockRequestsStatePublisher: CurrentValuePublisher<KnockRequestsState, Never> { get }
78-
85+
7986
var timeline: TimelineProxyProtocol { get }
8087

8188
var predecessorRoom: PredecessorRoom? { get }
@@ -170,6 +177,7 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
170177

171178
func elementCallWidgetDriver(deviceID: String) -> ElementCallWidgetDriverProtocol
172179
func declineCall(notificationId: String) async -> Result<Void, RoomProxyError>
180+
func callDeclineEventPublisher(notificationId rtcNotificationEventId: String) -> AnyPublisher<RtcDeclinedEvent, Never>
173181

174182
// MARK: - Permalinks
175183

0 commit comments

Comments
 (0)