Fixes #3050 - Sync mute state between ElementCall and CallKit

This commit is contained in:
Stefan Ceriu
2024-08-06 13:23:29 +03:00
committed by Stefan Ceriu
parent e667be0f43
commit d31e128aa7
7 changed files with 107 additions and 67 deletions
@@ -838,8 +838,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
private func startSync() { private func startSync() {
guard let userSession else { return } guard let userSession else { return }
// FIXME: replace this with `user_id_server_name` from https://github.com/matrix-org/matrix-rust-sdk/pull/3617 let serverName = String(userSession.clientProxy.userIDServerName ?? "Unknown")
let serverName = String(userSession.clientProxy.userID.split(separator: ":").last ?? "Unknown")
ServiceLocator.shared.analytics.signpost.beginFirstSync(serverName: serverName) ServiceLocator.shared.analytics.signpost.beginFirstSync(serverName: serverName)
userSession.clientProxy.startSync() userSession.clientProxy.startSync()
@@ -5007,17 +5007,17 @@ class ElementCallServiceMock: ElementCallServiceProtocol {
tearDownCallSessionCallsCount += 1 tearDownCallSessionCallsCount += 1
tearDownCallSessionClosure?() tearDownCallSessionClosure?()
} }
//MARK: - setCallMuted //MARK: - setAudioEnabled
var setCallMutedRoomIDUnderlyingCallsCount = 0 var setAudioEnabledRoomIDUnderlyingCallsCount = 0
var setCallMutedRoomIDCallsCount: Int { var setAudioEnabledRoomIDCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return setCallMutedRoomIDUnderlyingCallsCount return setAudioEnabledRoomIDUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = setCallMutedRoomIDUnderlyingCallsCount returnValue = setAudioEnabledRoomIDUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -5025,28 +5025,28 @@ class ElementCallServiceMock: ElementCallServiceProtocol {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
setCallMutedRoomIDUnderlyingCallsCount = newValue setAudioEnabledRoomIDUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
setCallMutedRoomIDUnderlyingCallsCount = newValue setAudioEnabledRoomIDUnderlyingCallsCount = newValue
} }
} }
} }
} }
var setCallMutedRoomIDCalled: Bool { var setAudioEnabledRoomIDCalled: Bool {
return setCallMutedRoomIDCallsCount > 0 return setAudioEnabledRoomIDCallsCount > 0
} }
var setCallMutedRoomIDReceivedArguments: (muted: Bool, roomID: String)? var setAudioEnabledRoomIDReceivedArguments: (enabled: Bool, roomID: String)?
var setCallMutedRoomIDReceivedInvocations: [(muted: Bool, roomID: String)] = [] var setAudioEnabledRoomIDReceivedInvocations: [(enabled: Bool, roomID: String)] = []
var setCallMutedRoomIDClosure: ((Bool, String) -> Void)? var setAudioEnabledRoomIDClosure: ((Bool, String) -> Void)?
func setCallMuted(_ muted: Bool, roomID: String) { func setAudioEnabled(_ enabled: Bool, roomID: String) {
setCallMutedRoomIDCallsCount += 1 setAudioEnabledRoomIDCallsCount += 1
setCallMutedRoomIDReceivedArguments = (muted: muted, roomID: roomID) setAudioEnabledRoomIDReceivedArguments = (enabled: enabled, roomID: roomID)
DispatchQueue.main.async { DispatchQueue.main.async {
self.setCallMutedRoomIDReceivedInvocations.append((muted: muted, roomID: roomID)) self.setAudioEnabledRoomIDReceivedInvocations.append((enabled: enabled, roomID: roomID))
} }
setCallMutedRoomIDClosure?(muted, roomID) setAudioEnabledRoomIDClosure?(enabled, roomID)
} }
} }
class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol { class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol {
@@ -62,9 +62,6 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
return return
} }
// TODO: intercept EC mute state changes and pass them over to CallKit
// elementCallService.setCallMuted(roomID: roomProxy.id, muted: muted)
Task { Task {
await self.widgetDriver.sendMessage(message) await self.widgetDriver.sendMessage(message)
} }
@@ -76,14 +73,14 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
guard let self else { return } guard let self else { return }
switch action { switch action {
case let .setCallMuted(muted, roomID): case let .setAudioEnabled(enabled, roomID):
guard roomID == roomProxy.id else { guard roomID == roomProxy.id else {
MXLog.error("Received mute request for a different room: \(roomID) != \(roomProxy.id)") MXLog.error("Received mute request for a different room: \(roomID) != \(roomProxy.id)")
return return
} }
Task { Task {
await self.setMuted(muted) await self.setAudioEnabled(enabled)
} }
default: default:
break break
@@ -97,13 +94,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
guard let self else { return } guard let self else { return }
Task { Task {
do { await self.postJSONToWidget(receivedMessage)
let message = "postMessage(\(receivedMessage), '*')"
let result = try await self.state.bindings.javaScriptEvaluator?(message)
MXLog.debug("Evaluated javascript: \(message) with result: \(String(describing: result))")
} catch {
MXLog.error("Received javascript evaluation error: \(error)")
}
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -116,6 +107,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
switch action { switch action {
case .callEnded: case .callEnded:
actionsSubject.send(.dismiss) actionsSubject.send(.dismiss)
case .mediaStateChanged(let audioEnabled, _):
elementCallService.setAudioEnabled(audioEnabled, roomID: roomProxy.id)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -143,8 +136,6 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
await elementCallService.setupCallSession(roomID: roomProxy.id, roomDisplayName: roomProxy.roomTitle) await elementCallService.setupCallSession(roomID: roomProxy.id, roomDisplayName: roomProxy.roomTitle)
// TODO: Pass over the current EC mute status to CallKit
let _ = await roomProxy.sendCallNotificationIfNeeeded() let _ = await roomProxy.sendCallNotificationIfNeeeded()
} }
} }
@@ -159,7 +150,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
func stop() { func stop() {
Task { Task {
await hangUp() await hangup()
} }
elementCallService.tearDownCallSession() elementCallService.tearDownCallSession()
@@ -167,23 +158,44 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
// MARK: - Private // MARK: - Private
private func setMuted(_ muted: Bool) async { private func setAudioEnabled(_ enabled: Bool) async {
// Not supported on EC yet let message = ElementCallWidgetMessage(direction: .toWidget,
action: .mediaState,
data: .init(audioEnabled: enabled),
widgetId: widgetDriver.widgetID)
await postMessageToWidget(message)
} }
private func hangUp() async { func hangup() async {
let hangUpMessage = """ let message = ElementCallWidgetMessage(direction: .fromWidget,
{"api":"fromWidget", action: .hangup,
"widgetId":"\(widgetDriver.widgetID)", widgetId: widgetDriver.widgetID)
"requestId":"widgetapi-\(UUID())",
"action":"im.vector.hangup",
"data":{}}
"""
let result = await widgetDriver.sendMessage(hangUpMessage) await postMessageToWidget(message)
MXLog.info("Sent hangUp message with result: \(result)")
} }
private func postMessageToWidget(_ message: ElementCallWidgetMessage) async {
do {
let data = try JSONEncoder().encode(message)
let json = String(decoding: data, as: UTF8.self)
_ = await widgetDriver.sendMessage(json)
await postJSONToWidget(json)
} catch {
MXLog.error("Failed encoding widget message with error: \(error)")
}
}
private func postJSONToWidget(_ json: String) async {
do {
let message = "postMessage(\(json), '*')"
let result = try await state.bindings.javaScriptEvaluator?(message)
MXLog.debug("Evaluated javascript: \(json) with result: \(String(describing: result))")
} catch {
MXLog.error("Received javascript evaluation error: \(error)")
}
}
private static let eventHandlerName = "elementx" private static let eventHandlerName = "elementx"
private static var eventHandlerInjectionScript: String { private static var eventHandlerInjectionScript: String {
@@ -118,7 +118,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
tearDownCallSession(sendEndCallAction: true) tearDownCallSession(sendEndCallAction: true)
} }
func setCallMuted(_ muted: Bool, roomID: String) { func setAudioEnabled(_ enabled: Bool, roomID: String) {
guard let ongoingCallID else { guard let ongoingCallID else {
MXLog.error("Failed toggling call microphone, no calls running") MXLog.error("Failed toggling call microphone, no calls running")
return return
@@ -129,7 +129,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
return return
} }
let transaction = CXTransaction(action: CXSetMutedCallAction(call: ongoingCallID.callKitID, muted: muted)) let transaction = CXTransaction(action: CXSetMutedCallAction(call: ongoingCallID.callKitID, muted: !enabled))
callController.request(transaction) { error in callController.request(transaction) { error in
if let error { if let error {
MXLog.error("Failed toggling call microphone with error: \(error)") MXLog.error("Failed toggling call microphone with error: \(error)")
@@ -211,16 +211,13 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
} }
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
// if let ongoingCallID { if let ongoingCallID {
// actionsSubject.send(.setCallMuted(action.isMuted, roomID: ongoingCallID.roomID)) actionsSubject.send(.setAudioEnabled(!action.isMuted, roomID: ongoingCallID.roomID))
// } else { } else {
// MXLog.error("Failed muting/unmuting call, missing ongoingCallID") 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.fulfill()
action.fail()
} }
func provider(_ provider: CXProvider, perform action: CXEndCallAction) { func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
@@ -19,7 +19,7 @@ import Combine
enum ElementCallServiceAction { enum ElementCallServiceAction {
case startCall(roomID: String) case startCall(roomID: String)
case endCall(roomID: String) case endCall(roomID: String)
case setCallMuted(_ muted: Bool, roomID: String) case setAudioEnabled(_ enabled: Bool, roomID: String)
} }
// sourcery: AutoMockable // sourcery: AutoMockable
@@ -32,5 +32,5 @@ protocol ElementCallServiceProtocol {
func tearDownCallSession() func tearDownCallSession()
func setCallMuted(_ muted: Bool, roomID: String) func setAudioEnabled(_ enabled: Bool, roomID: String)
} }
@@ -18,7 +18,7 @@ import Combine
import MatrixRustSDK import MatrixRustSDK
import SwiftUI import SwiftUI
private struct ElementCallWidgetMessage: Codable { struct ElementCallWidgetMessage: Codable {
enum Direction: String, Codable { enum Direction: String, Codable {
case fromWidget case fromWidget
case toWidget case toWidget
@@ -26,14 +26,32 @@ private struct ElementCallWidgetMessage: Codable {
enum Action: String, Codable { enum Action: String, Codable {
case hangup = "im.vector.hangup" case hangup = "im.vector.hangup"
case mediaState = "io.element.device_mute"
}
struct Data: Codable {
var audioEnabled: Bool?
var videoEnabled: Bool?
enum CodingKeys: String, CodingKey {
case audioEnabled = "audio_enabled"
case videoEnabled = "video_enabled"
}
} }
let direction: Direction let direction: Direction
let action: Action let action: Action
var data: Data = .init()
let widgetId: String
var requestId = "widgetapi-\(UUID())"
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case direction = "api" case direction = "api"
case action case action
case data
case widgetId
case requestId
} }
} }
@@ -151,16 +169,29 @@ class ElementCallWidgetDriver: WidgetCapabilitiesProvider, ElementCallWidgetDriv
// MARK: - Private // MARK: - Private
func handleMessageIfNeeded(_ message: String) { func handleMessageIfNeeded(_ message: String) {
guard let data = message.data(using: .utf8), guard let data = message.data(using: .utf8) else {
let widgetMessage = try? JSONDecoder().decode(ElementCallWidgetMessage.self, from: data) else {
return return
} }
if widgetMessage.direction == .fromWidget { do {
switch widgetMessage.action { let widgetMessage = try JSONDecoder().decode(ElementCallWidgetMessage.self, from: data)
case .hangup: if widgetMessage.direction == .fromWidget {
actionsSubject.send(.callEnded) switch widgetMessage.action {
case .hangup:
actionsSubject.send(.callEnded)
case .mediaState:
guard let audioEnabled = widgetMessage.data.audioEnabled,
let videoEnabled = widgetMessage.data.videoEnabled else {
MXLog.error("Media state change messages should contain info data")
return
}
actionsSubject.send(.mediaStateChanged(audioEnabled: audioEnabled, videoEnabled: videoEnabled))
}
} }
} catch {
// Not all actions are supported
MXLog.verbose("Failed processing widget message with error: \(error)")
} }
} }
} }
@@ -28,6 +28,7 @@ enum ElementCallWidgetDriverError: Error {
enum ElementCallWidgetDriverAction { enum ElementCallWidgetDriverAction {
case callEnded case callEnded
case mediaStateChanged(audioEnabled: Bool, videoEnabled: Bool)
} }
// sourcery: AutoMockable // sourcery: AutoMockable