mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 20:09:04 +00:00
Fixes #3050 - Sync mute state between ElementCall and CallKit
This commit is contained in:
committed by
Stefan Ceriu
parent
e667be0f43
commit
d31e128aa7
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user