From d16ba9f3ef312bbae23ad00daffdeecea3ab73ca Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Thu, 27 Jun 2024 14:33:16 +0300 Subject: [PATCH] Hook up CallKit lock screen muting controls to the ElementCall widget --- .../Sources/Application/AppCoordinator.swift | 4 +- .../UserSessionFlowCoordinator.swift | 4 +- .../Mocks/Generated/GeneratedMocks.swift | 43 ++++++++++++++++++- .../CallScreen/CallScreenViewModel.swift | 30 +++++++++++++ .../ElementCall/ElementCallService.swift | 32 ++++++++++++-- .../ElementCallServiceProtocol.swift | 3 ++ 6 files changed, 108 insertions(+), 8 deletions(-) diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index e710506b8..ec45fde8a 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -157,8 +157,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg switch action { case .startCall(let roomID): self?.handleAppRoute(.call(roomID: roomID)) - case .endCall: - break // Handled internally in the UserSessionFlowCoordinator + default: + break } } .store(in: &cancellables) diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 935ade886..d8bfe4622 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -173,10 +173,10 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { .receive(on: DispatchQueue.main) .sink { [weak self] action in switch action { - case .startCall: - break case .endCall: self?.dismissCallScreenIfNeeded() + default: + break } } .store(in: &cancellables) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 13b963963..bf6676b30 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 2.2.4 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 2.2.5 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT // swiftlint:disable all @@ -4902,6 +4902,47 @@ class ElementCallServiceMock: ElementCallServiceProtocol { tearDownCallSessionCallsCount += 1 tearDownCallSessionClosure?() } + //MARK: - setCallMuted + + var setCallMutedRoomIDUnderlyingCallsCount = 0 + var setCallMutedRoomIDCallsCount: Int { + get { + if Thread.isMainThread { + return setCallMutedRoomIDUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = setCallMutedRoomIDUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + setCallMutedRoomIDUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + setCallMutedRoomIDUnderlyingCallsCount = newValue + } + } + } + } + var setCallMutedRoomIDCalled: Bool { + return setCallMutedRoomIDCallsCount > 0 + } + var setCallMutedRoomIDReceivedArguments: (muted: Bool, roomID: String)? + var setCallMutedRoomIDReceivedInvocations: [(muted: Bool, roomID: String)] = [] + var setCallMutedRoomIDClosure: ((Bool, String) -> Void)? + + func setCallMuted(_ muted: Bool, roomID: String) { + setCallMutedRoomIDCallsCount += 1 + setCallMutedRoomIDReceivedArguments = (muted: muted, roomID: roomID) + DispatchQueue.main.async { + self.setCallMutedRoomIDReceivedInvocations.append((muted: muted, roomID: roomID)) + } + setCallMutedRoomIDClosure?(muted, roomID) + } } class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol { var widgetID: String { diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift index 38bd48574..1ca32e37c 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift @@ -58,11 +58,35 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol return } + // TODO: intercept EC mute state changes and pass them over to CallKit + // elementCallService.setCallMuted(roomID: roomProxy.id, muted: muted) + Task { await self.widgetDriver.sendMessage(message) } } + elementCallService.actions + .receive(on: DispatchQueue.main) + .sink { [weak self] action in + guard let self else { return } + + switch action { + case let .setCallMuted(muted, roomID): + guard roomID == roomProxy.id else { + MXLog.error("Received mute request for a different room: \(roomID) != \(roomProxy.id)") + return + } + + Task { + await self.setMuted(muted) + } + default: + break + } + } + .store(in: &cancellables) + widgetDriver.messagePublisher .receive(on: DispatchQueue.main) .sink { [weak self] receivedMessage in @@ -115,6 +139,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol await elementCallService.setupCallSession(roomID: roomProxy.id, roomDisplayName: roomProxy.roomTitle) + // TODO: Pass over the current EC mute status to CallKit + let _ = await roomProxy.sendCallNotificationIfNeeeded() } } @@ -137,6 +163,10 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol // MARK: - Private + private func setMuted(_ muted: Bool) async { + // Not supported on EC yet + } + private func hangUp() async { let hangUpMessage = """ {"api":"fromWidget", diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index 48612c6f2..2502ecb97 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -103,6 +103,25 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe func tearDownCallSession() { tearDownCallSession(sendEndCallAction: true) } + + func setCallMuted(_ muted: Bool, roomID: String) { + guard let ongoingCallID else { + MXLog.error("Failed toggling call microphone, no calls running") + return + } + + guard ongoingCallID.roomID == roomID else { + MXLog.error("Failed toggling call microphone, rooms don't match: \(ongoingCallID.roomID) != \(roomID)") + return + } + + let transaction = CXTransaction(action: CXSetMutedCallAction(call: ongoingCallID.callKitID, muted: muted)) + callController.request(transaction) { error in + if let error { + MXLog.error("Failed toggling call microphone with error: \(error)") + } + } + } // MARK: - PKPushRegistryDelegate @@ -164,9 +183,16 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { - // Forward this to the widget somehow - // webView.evaluateJavaScript("groupCall.setLocalVideoMuted(!groupCall.isLocalVideoMuted())") - // webView.evaluateJavaScript("groupCall.setMicrophoneMuted(!groupCall.isMicrophoneMuted())" + // if let ongoingCallID { + // actionsSubject.send(.setCallMuted(action.isMuted, roomID: ongoingCallID.roomID)) + // } else { + // MXLog.error("Failed muting/unmuting call, missing ongoingCallID") + // } + // + // action.fulfill() + + // TODO: EC doesn't expose controls for this yet. Fail the action for now. + action.fail() } func provider(_ provider: CXProvider, perform action: CXEndCallAction) { diff --git a/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift b/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift index 06793f42e..54557e6b0 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallServiceProtocol.swift @@ -19,6 +19,7 @@ import Combine enum ElementCallServiceAction { case startCall(roomID: String) case endCall(roomID: String) + case setCallMuted(_ muted: Bool, roomID: String) } enum ElementCallServiceNotificationKey: String { @@ -35,4 +36,6 @@ protocol ElementCallServiceProtocol { func setupCallSession(roomID: String, roomDisplayName: String) async func tearDownCallSession() + + func setCallMuted(_ muted: Bool, roomID: String) }