From 897b806c254ec644ef20d82ae33b48e86446e4ce Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 16 Jul 2024 17:09:16 +0300 Subject: [PATCH] Fixes #3042 - Cancel ElementCall ringing as soon as the call ends --- .../Sources/Application/AppCoordinator.swift | 9 +++ .../Mocks/Generated/GeneratedMocks.swift | 76 +++++++++++++++++++ .../ElementCall/ElementCallService.swift | 58 +++++++++++++- .../ElementCallServiceConstants.swift | 24 ++++++ .../ElementCallServiceProtocol.swift | 9 +-- .../Sources/Services/Room/RoomProxy.swift | 14 ++-- .../Services/Room/RoomProxyProtocol.swift | 2 + NSE/SupportingFiles/target.yml | 2 +- 8 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index ec45fde8a0..70e5b7467e 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -42,6 +42,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg didSet { userSessionObserver?.cancel() if userSession != nil { + configureElementCallService() configureNotificationManager() observeUserSessionChanges() startSync() @@ -637,6 +638,14 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } } } + + private func configureElementCallService() { + guard let userSession else { + fatalError("User session not setup") + } + + elementCallService.setClientProxy(userSession.clientProxy) + } private func configureNotificationManager() { notificationManager.setUserSession(userSession) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index bf6676b302..16adb7c272 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -4826,6 +4826,47 @@ class ElementCallServiceMock: ElementCallServiceProtocol { } var underlyingActions: AnyPublisher! + //MARK: - setClientProxy + + var setClientProxyUnderlyingCallsCount = 0 + var setClientProxyCallsCount: Int { + get { + if Thread.isMainThread { + return setClientProxyUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = setClientProxyUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + setClientProxyUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + setClientProxyUnderlyingCallsCount = newValue + } + } + } + } + var setClientProxyCalled: Bool { + return setClientProxyCallsCount > 0 + } + var setClientProxyReceivedClientProxy: ClientProxyProtocol? + var setClientProxyReceivedInvocations: [ClientProxyProtocol] = [] + var setClientProxyClosure: ((ClientProxyProtocol) -> Void)? + + func setClientProxy(_ clientProxy: ClientProxyProtocol) { + setClientProxyCallsCount += 1 + setClientProxyReceivedClientProxy = clientProxy + DispatchQueue.main.async { + self.setClientProxyReceivedInvocations.append(clientProxy) + } + setClientProxyClosure?(clientProxy) + } //MARK: - setupCallSession var setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount = 0 @@ -8300,6 +8341,41 @@ class RoomProxyMock: RoomProxyProtocol { subscribeForUpdatesCallsCount += 1 await subscribeForUpdatesClosure?() } + //MARK: - subscribeToRoomInfoUpdates + + var subscribeToRoomInfoUpdatesUnderlyingCallsCount = 0 + var subscribeToRoomInfoUpdatesCallsCount: Int { + get { + if Thread.isMainThread { + return subscribeToRoomInfoUpdatesUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = subscribeToRoomInfoUpdatesUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + subscribeToRoomInfoUpdatesUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + subscribeToRoomInfoUpdatesUnderlyingCallsCount = newValue + } + } + } + } + var subscribeToRoomInfoUpdatesCalled: Bool { + return subscribeToRoomInfoUpdatesCallsCount > 0 + } + var subscribeToRoomInfoUpdatesClosure: (() -> Void)? + + func subscribeToRoomInfoUpdates() { + subscribeToRoomInfoUpdatesCallsCount += 1 + subscribeToRoomInfoUpdatesClosure?() + } //MARK: - timelineFocusedOnEvent var timelineFocusedOnEventEventIDNumberOfEventsUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index 2502ecb97f..be1e305a79 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -44,7 +44,17 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe return CXProvider(configuration: configuration) }() - private var incomingCallID: CallID? + private weak var clientProxy: ClientProxyProtocol? + + private var cancellables = Set() + private var incomingCallID: CallID? { + didSet { + Task { + await observeIncomingCallRoomStateUpdates() + } + } + } + private var endUnansweredCallTask: Task? private var ongoingCallID: CallID? @@ -65,6 +75,10 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe callProvider.setDelegate(self, queue: nil) } + func setClientProxy(_ clientProxy: any ClientProxyProtocol) { + self.clientProxy = clientProxy + } + func setupCallSession(roomID: String, roomDisplayName: String) async { // Drop any ongoing calls when starting a new one if ongoingCallID != nil { @@ -221,4 +235,46 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe ongoingCallID = nil } + + func observeIncomingCallRoomStateUpdates() async { + cancellables.removeAll() + + guard let clientProxy, let incomingCallID else { + return + } + + guard let roomProxy = await clientProxy.roomForIdentifier(incomingCallID.roomID) else { + 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.hasOngoingCall + + roomProxy + .actionsPublisher + .map { action in + switch action { + case .roomInfoUpdate: + return roomProxy.hasOngoingCall + } + } + .removeDuplicates() + .dropFirst(isCallOngoing ? 0 : 1) + .sink { [weak self] hasOngoingCall in + guard let self else { return } + + if !hasOngoingCall { + MXLog.info("Call has been cancelled") + cancellables.removeAll() + endUnansweredCallTask?.cancel() + callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .remoteEnded) + } + } + .store(in: &cancellables) + } } diff --git a/ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift b/ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift new file mode 100644 index 0000000000..bec2dc85d2 --- /dev/null +++ b/ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift @@ -0,0 +1,24 @@ +// +// Copyright 2024 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum ElementCallServiceNotificationKey: String { + case roomID + case roomDisplayName +} + +let ElementCallServiceNotificationDiscardDelta = 10.0 diff --git a/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift b/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift index 54557e6b09..d295a956d6 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift @@ -22,17 +22,12 @@ enum ElementCallServiceAction { case setCallMuted(_ muted: Bool, roomID: String) } -enum ElementCallServiceNotificationKey: String { - case roomID - case roomDisplayName -} - -let ElementCallServiceNotificationDiscardDelta = 10.0 - // sourcery: AutoMockable protocol ElementCallServiceProtocol { var actions: AnyPublisher { get } + func setClientProxy(_ clientProxy: ClientProxyProtocol) + func setupCallSession(roomID: String, roomDisplayName: String) async func tearDownCallSession() diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index dbb27ce847..9f84778b2c 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -155,6 +155,13 @@ class RoomProxy: RoomProxyProtocol { subscribeToTypingNotifications() } + func subscribeToRoomInfoUpdates() { + roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] in + MXLog.info("Received room info update") + self?.actionsSubject.send(.roomInfoUpdate) + }) + } + func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result { do { let timeline = try await room.timelineFocusedOnEvent(eventId: eventID, numContextEvents: numberOfEvents, internalIdPrefix: UUID().uuidString) @@ -596,13 +603,6 @@ class RoomProxy: RoomProxyProtocol { // MARK: - Private - private func subscribeToRoomInfoUpdates() { - roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] in - MXLog.info("Received room info update") - self?.actionsSubject.send(.roomInfoUpdate) - }) - } - private func subscribeToTypingNotifications() { typingNotificationObservationToken = room.subscribeToTypingNotifications(listener: RoomTypingNotificationUpdateListener { [weak self] typingUserIDs in guard let self else { return } diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index bcba50ca10..e79f687750 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -66,6 +66,8 @@ protocol RoomProxyProtocol { func subscribeForUpdates() async + func subscribeToRoomInfoUpdates() + func timelineFocusedOnEvent(eventID: String, numberOfEvents: UInt16) async -> Result func redact(_ eventID: String) async -> Result diff --git a/NSE/SupportingFiles/target.yml b/NSE/SupportingFiles/target.yml index 83284e96fa..bad92c044d 100644 --- a/NSE/SupportingFiles/target.yml +++ b/NSE/SupportingFiles/target.yml @@ -105,5 +105,5 @@ targets: - path: ../../ElementX/Sources/Services/Notification/Proxy - path: ../../ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift - path: ../../ElementX/Sources/Services/UserSession/RestorationToken.swift - - path: ../../ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift + - path: ../../ElementX/Sources/Services/ElementCall/ElementCallServiceConstants.swift - path: ../../ElementX/Sources/Application/AppSettings.swift