Cleanup how we setup the CallKit provider and have it be used for outgoing calls as well (#2967)

- tear down ElementCall screens when ending the call from the CXCallController
- make the call UI available in the task manager and lock screen
- Fix broken hang up widget message format
This commit is contained in:
Stefan Ceriu
2024-06-27 14:07:44 +03:00
committed by GitHub
parent 152ecc6f9c
commit 41d40d8de0
11 changed files with 182 additions and 97 deletions
+4
View File
@@ -605,6 +605,7 @@
8DCD9CC5361FF22A5B2C20F1 /* AppLockSetupSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9FCE4D1E3A81AC1CC5CB91 /* AppLockSetupSettingsScreenCoordinator.swift */; }; 8DCD9CC5361FF22A5B2C20F1 /* AppLockSetupSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9FCE4D1E3A81AC1CC5CB91 /* AppLockSetupSettingsScreenCoordinator.swift */; };
8DDC6F28C797D8685F2F8E32 /* AnalyticsConsentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */; }; 8DDC6F28C797D8685F2F8E32 /* AnalyticsConsentState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57B6B383F1FD04CC0E7B60C6 /* AnalyticsConsentState.swift */; };
8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */; }; 8E650379587C31D7912ED67B /* UNNotification+Creator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */; };
8E7A902CA16E24928F83646C /* ElementCallServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */; };
8ED8AF57A06F5EE9978ED23F /* AuthenticationStartScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */; }; 8ED8AF57A06F5EE9978ED23F /* AuthenticationStartScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FB89DC7F9A4A91020037001 /* AuthenticationStartScreenViewModelTests.swift */; };
8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */; }; 8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */; };
8F2FAA98457750D9D664136F /* Mapbox in Frameworks */ = {isa = PBXBuildFile; productRef = C1BF15833233CD3BDB7E2B1D /* Mapbox */; }; 8F2FAA98457750D9D664136F /* Mapbox in Frameworks */ = {isa = PBXBuildFile; productRef = C1BF15833233CD3BDB7E2B1D /* Mapbox */; };
@@ -2052,6 +2053,7 @@
E2DCA495ED42D2463DDAA94D /* TimelineBubbleLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBubbleLayout.swift; sourceTree = "<group>"; }; E2DCA495ED42D2463DDAA94D /* TimelineBubbleLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBubbleLayout.swift; sourceTree = "<group>"; };
E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyMock.swift; sourceTree = "<group>"; }; E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyMock.swift; sourceTree = "<group>"; };
E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModel.swift; sourceTree = "<group>"; }; E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModel.swift; sourceTree = "<group>"; };
E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceMock.swift; sourceTree = "<group>"; };
E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; }; E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = "<group>"; }; E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = "<group>"; };
E4103AB4340F2974D690A12A /* CallScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreen.swift; sourceTree = "<group>"; }; E4103AB4340F2974D690A12A /* CallScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreen.swift; sourceTree = "<group>"; };
@@ -2691,6 +2693,7 @@
3BAC027034248429A438886B /* AppMediatorMock.swift */, 3BAC027034248429A438886B /* AppMediatorMock.swift */,
E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */, E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */,
4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */, 4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */,
E321E840DCC63790049984F4 /* ElementCallServiceMock.swift */,
382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */, 382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */,
B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */, B2AD8A56CD37E23071A2F4BF /* PHGPostHogMock.swift */,
D38391154120264910D19528 /* PollMock.swift */, D38391154120264910D19528 /* PollMock.swift */,
@@ -6041,6 +6044,7 @@
AE1160076F663BF14E0E893A /* EffectsView.swift in Sources */, AE1160076F663BF14E0E893A /* EffectsView.swift in Sources */,
FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */, FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */,
5732395A4F71F51F9C754C5A /* ElementCallService.swift in Sources */, 5732395A4F71F51F9C754C5A /* ElementCallService.swift in Sources */,
8E7A902CA16E24928F83646C /* ElementCallServiceMock.swift in Sources */,
48416BBEB8DDF3E4DED0EDB6 /* ElementCallServiceProtocol.swift in Sources */, 48416BBEB8DDF3E4DED0EDB6 /* ElementCallServiceProtocol.swift in Sources */,
07CC13C5729C24255348CBBD /* ElementCallWidgetDriver.swift in Sources */, 07CC13C5729C24255348CBBD /* ElementCallWidgetDriver.swift in Sources */,
370AF5BFCD4384DD455479B6 /* ElementCallWidgetDriverProtocol.swift in Sources */, 370AF5BFCD4384DD455479B6 /* ElementCallWidgetDriverProtocol.swift in Sources */,
@@ -149,10 +149,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] action in .sink { [weak self] action in
switch action { switch action {
case .answerCall(let roomID): case .startCall(let roomID):
self?.handleAppRoute(.call(roomID: roomID)) self?.handleAppRoute(.call(roomID: roomID))
case .declineCall: case .endCall:
break break // Handled internally in the UserSessionFlowCoordinator
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@@ -168,6 +168,18 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
elementCallService.actions
.receive(on: DispatchQueue.main)
.sink { [weak self] action in
switch action {
case .startCall:
break
case .endCall:
self?.dismissCallScreenIfNeeded()
}
}
.store(in: &cancellables)
} }
func start() { func start() {
@@ -569,6 +581,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
presentCallScreen(roomProxy: roomProxy) presentCallScreen(roomProxy: roomProxy)
} }
private func dismissCallScreenIfNeeded() {
guard navigationSplitCoordinator.sheetCoordinator is CallScreenCoordinator else {
return
}
navigationSplitCoordinator.setSheetCoordinator(nil)
}
// MARK: Secure backup confirmation // MARK: Secure backup confirmation
private func presentSecureBackupLogoutConfirmationScreen() { private func presentSecureBackupLogoutConfirmationScreen() {
@@ -0,0 +1,28 @@
//
// 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 Combine
import Foundation
struct ElementCallServiceMockConfiguration { }
extension ElementCallServiceMock {
convenience init(_ configuration: ElementCallServiceMockConfiguration) {
self.init()
underlyingActions = PassthroughSubject().eraseToAnyPublisher()
}
}
@@ -4697,15 +4697,15 @@ class ElementCallServiceMock: ElementCallServiceProtocol {
//MARK: - setupCallSession //MARK: - setupCallSession
var setupCallSessionTitleUnderlyingCallsCount = 0 var setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount = 0
var setupCallSessionTitleCallsCount: Int { var setupCallSessionRoomIDRoomDisplayNameCallsCount: Int {
get { get {
if Thread.isMainThread { if Thread.isMainThread {
return setupCallSessionTitleUnderlyingCallsCount return setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount
} else { } else {
var returnValue: Int? = nil var returnValue: Int? = nil
DispatchQueue.main.sync { DispatchQueue.main.sync {
returnValue = setupCallSessionTitleUnderlyingCallsCount returnValue = setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount
} }
return returnValue! return returnValue!
@@ -4713,28 +4713,28 @@ class ElementCallServiceMock: ElementCallServiceProtocol {
} }
set { set {
if Thread.isMainThread { if Thread.isMainThread {
setupCallSessionTitleUnderlyingCallsCount = newValue setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount = newValue
} else { } else {
DispatchQueue.main.sync { DispatchQueue.main.sync {
setupCallSessionTitleUnderlyingCallsCount = newValue setupCallSessionRoomIDRoomDisplayNameUnderlyingCallsCount = newValue
} }
} }
} }
} }
var setupCallSessionTitleCalled: Bool { var setupCallSessionRoomIDRoomDisplayNameCalled: Bool {
return setupCallSessionTitleCallsCount > 0 return setupCallSessionRoomIDRoomDisplayNameCallsCount > 0
} }
var setupCallSessionTitleReceivedTitle: String? var setupCallSessionRoomIDRoomDisplayNameReceivedArguments: (roomID: String, roomDisplayName: String)?
var setupCallSessionTitleReceivedInvocations: [String] = [] var setupCallSessionRoomIDRoomDisplayNameReceivedInvocations: [(roomID: String, roomDisplayName: String)] = []
var setupCallSessionTitleClosure: ((String) async -> Void)? var setupCallSessionRoomIDRoomDisplayNameClosure: ((String, String) async -> Void)?
func setupCallSession(title: String) async { func setupCallSession(roomID: String, roomDisplayName: String) async {
setupCallSessionTitleCallsCount += 1 setupCallSessionRoomIDRoomDisplayNameCallsCount += 1
setupCallSessionTitleReceivedTitle = title setupCallSessionRoomIDRoomDisplayNameReceivedArguments = (roomID: roomID, roomDisplayName: roomDisplayName)
DispatchQueue.main.async { DispatchQueue.main.async {
self.setupCallSessionTitleReceivedInvocations.append(title) self.setupCallSessionRoomIDRoomDisplayNameReceivedInvocations.append((roomID: roomID, roomDisplayName: roomDisplayName))
} }
await setupCallSessionTitleClosure?(title) await setupCallSessionRoomIDRoomDisplayNameClosure?(roomID, roomDisplayName)
} }
//MARK: - tearDownCallSession //MARK: - tearDownCallSession
@@ -102,7 +102,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
return return
} }
await elementCallService.setupCallSession(title: roomProxy.roomTitle) await elementCallService.setupCallSession(roomID: roomProxy.id, roomDisplayName: roomProxy.roomTitle)
let _ = await roomProxy.sendCallNotificationIfNeeeded() let _ = await roomProxy.sendCallNotificationIfNeeeded()
} }
@@ -128,15 +128,15 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
private func hangUp() async { private func hangUp() async {
let hangUpMessage = """ let hangUpMessage = """
"api":"toWidget", {"api":"fromWidget",
"widgetId":"\(widgetDriver.widgetID)", "widgetId":"\(widgetDriver.widgetID)",
"requestId":"widgetapi-\(UUID())", "requestId":"widgetapi-\(UUID())",
"action":"im.vector.hangup", "action":"im.vector.hangup",
"data":{} "data":{}}
""" """
let result = await widgetDriver.sendMessage(hangUpMessage) let result = await widgetDriver.sendMessage(hangUpMessage)
MXLog.error("Result yo: \(result)") MXLog.info("Sent hangUp message with result: \(result)")
} }
private static let eventHandlerName = "elementx" private static let eventHandlerName = "elementx"
@@ -181,7 +181,7 @@ struct CallScreen_Previews: PreviewProvider {
roomProxy.elementCallWidgetDriverReturnValue = widgetDriver roomProxy.elementCallWidgetDriverReturnValue = widgetDriver
return CallScreenViewModel(elementCallService: ElementCallServiceMock(), return CallScreenViewModel(elementCallService: ElementCallServiceMock(.init()),
roomProxy: roomProxy, roomProxy: roomProxy,
callBaseURL: "https://call.element.io", callBaseURL: "https://call.element.io",
clientID: "io.element.elementx") clientID: "io.element.elementx")
@@ -19,19 +19,36 @@ import CallKit
import Combine import Combine
import Foundation import Foundation
import PushKit import PushKit
import UIKit
class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDelegate, CXProviderDelegate { class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDelegate, CXProviderDelegate {
private struct CallID: Equatable {
let callKitID: UUID
let roomID: String
}
private let pushRegistry: PKPushRegistry private let pushRegistry: PKPushRegistry
private let callController = CXCallController() private let callController = CXCallController()
private let callProvider: CXProvider = {
private var callProvider: CXProvider? let configuration = CXProviderConfiguration()
private var ongoingCallID: UUID? configuration.supportsVideo = true
configuration.includesCallsInRecents = true
private var incomingCallRoomID: String?
if let callKitIcon = UIImage(named: "images/app-logo") {
configuration.iconTemplateImageData = callKitIcon.pngData()
}
// https://stackoverflow.com/a/46077628/730924
configuration.supportedHandleTypes = [.generic]
return CXProvider(configuration: configuration)
}()
private var incomingCallID: CallID?
private var endUnansweredCallTask: Task<Void, Never>? private var endUnansweredCallTask: Task<Void, Never>?
private var ongoingCallID: CallID?
private let actionsSubject: PassthroughSubject<ElementCallServiceAction, Never> = .init() private let actionsSubject: PassthroughSubject<ElementCallServiceAction, Never> = .init()
var actions: AnyPublisher<ElementCallServiceAction, Never> { var actions: AnyPublisher<ElementCallServiceAction, Never> {
actionsSubject.eraseToAnyPublisher() actionsSubject.eraseToAnyPublisher()
@@ -44,47 +61,47 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
pushRegistry.delegate = self pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.voIP] pushRegistry.desiredPushTypes = [.voIP]
callProvider.setDelegate(self, queue: nil)
} }
func setupCallSession(title: String) async { func setupCallSession(roomID: String, roomDisplayName: String) async {
guard ongoingCallID == nil else { // Drop any ongoing calls when starting a new one
return if ongoingCallID != nil {
tearDownCallSession()
} }
let callID = UUID() // If this starting from a ring reuse those identifiers
// Make sure the roomID matches
let callID = if let incomingCallID, incomingCallID.roomID == roomID {
incomingCallID
} else {
CallID(callKitID: UUID(), roomID: roomID)
}
incomingCallID = nil
ongoingCallID = callID ongoingCallID = callID
let handle = CXHandle(type: .generic, value: title) let handle = CXHandle(type: .generic, value: roomDisplayName)
let startCallAction = CXStartCallAction(call: callID, handle: handle) let startCallAction = CXStartCallAction(call: callID.callKitID, handle: handle)
startCallAction.isVideo = true startCallAction.isVideo = true
let transaction = CXTransaction(action: startCallAction)
do { do {
try await callController.request(transaction) try await callController.request(CXTransaction(action: startCallAction))
} catch {
MXLog.error("Failed requesting start call action with error: \(error)")
}
do { // Setup the audio session even if setting up CallKit failed, ElementCall **is** running at this point
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .videoChat, options: []) try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .videoChat, options: [])
try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation) try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
} catch { } catch {
MXLog.error("Failed setting up VoIP session with error: \(error)") MXLog.error("Failed setting up audio session with error: \(error)")
tearDownCallSession()
} }
} }
func tearDownCallSession() { func tearDownCallSession() {
guard let ongoingCallID else { tearDownCallSession(sendEndCallAction: true)
return
}
try? AVAudioSession.sharedInstance().setActive(false)
let endCallAction = CXEndCallAction(call: ongoingCallID)
let transaction = CXTransaction(action: endCallAction)
callController.request(transaction) { error in
if let error {
MXLog.error("Failed transaction with error: \(error)")
}
}
} }
// MARK: - PKPushRegistryDelegate // MARK: - PKPushRegistryDelegate
@@ -97,31 +114,18 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
return return
} }
let callID = UUID() let callID = CallID(callKitID: UUID(), roomID: roomID)
ongoingCallID = callID incomingCallID = callID
incomingCallRoomID = roomID let roomDisplayName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String
let configuration = CXProviderConfiguration()
configuration.supportsVideo = true
configuration.includesCallsInRecents = true
// Provide image icon if available
configuration.iconTemplateImageData = nil
// https://stackoverflow.com/a/46077628/730924
configuration.supportedHandleTypes = [.generic]
let update = CXCallUpdate() let update = CXCallUpdate()
update.hasVideo = true update.hasVideo = true
update.localizedCallerName = roomDisplayName
update.localizedCallerName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String
// https://stackoverflow.com/a/41230020/730924 // https://stackoverflow.com/a/41230020/730924
update.remoteHandle = .init(type: .generic, value: roomID) update.remoteHandle = .init(type: .generic, value: roomID)
let callProvider = CXProvider(configuration: configuration) callProvider.reportNewIncomingCall(with: callID.callKitID, update: update) { error in
callProvider.setDelegate(self, queue: nil)
callProvider.reportNewIncomingCall(with: callID, update: update) { error in
if let error { if let error {
MXLog.error("Failed reporting new incoming call with error: \(error)") MXLog.error("Failed reporting new incoming call with error: \(error)")
} }
@@ -129,14 +133,15 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
completion() completion()
} }
endUnansweredCallTask = Task { [weak self, callProvider, callID] in endUnansweredCallTask = Task { [weak self] in
try? await Task.sleep(for: .seconds(15)) try? await Task.sleep(for: .seconds(15))
guard !Task.isCancelled else {
guard let self, !Task.isCancelled else {
return return
} }
if let ongoingCallID = self?.ongoingCallID, ongoingCallID == callID { if let incomingCallID, incomingCallID.callKitID == callID.callKitID {
callProvider.reportCall(with: callID, endedAt: .now, reason: .unanswered) callProvider.reportCall(with: incomingCallID.callKitID, endedAt: nil, reason: .unanswered)
} }
} }
} }
@@ -147,29 +152,57 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
MXLog.info("Call provider did reset: \(provider)") MXLog.info("Call provider did reset: \(provider)")
} }
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
if let ongoingCallID {
provider.reportOutgoingCall(with: ongoingCallID.callKitID, connectedAt: nil)
} else {
MXLog.error("Failed starting call, missing ongoingCallID")
}
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
if let incomingCallRoomID { if let incomingCallID {
Task { actionsSubject.send(.startCall(roomID: incomingCallID.roomID))
// Dispatch to next run loop so it doesn't conflict with `setupCallSession`
actionsSubject.send(.answerCall(roomID: incomingCallRoomID))
}
self.incomingCallRoomID = nil
endUnansweredCallTask?.cancel() endUnansweredCallTask?.cancel()
} else { } else {
MXLog.error("Failed answering incoming call, missing room ID") MXLog.error("Failed answering incoming call, missing incomingCallID")
} }
action.fulfill() action.fulfill()
} }
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())"
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) { func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
if let incomingCallRoomID { if let ongoingCallID {
actionsSubject.send(.declineCall(roomID: incomingCallRoomID)) actionsSubject.send(.endCall(roomID: ongoingCallID.roomID))
self.incomingCallRoomID = nil
} else {
MXLog.error("Failed declining incoming call, missing room ID")
} }
tearDownCallSession(sendEndCallAction: false)
action.fulfill() action.fulfill()
} }
// MARK: - Private
func tearDownCallSession(sendEndCallAction: Bool = true) {
try? AVAudioSession.sharedInstance().setActive(false)
if sendEndCallAction, let ongoingCallID {
let transaction = CXTransaction(action: CXEndCallAction(call: ongoingCallID.callKitID))
callController.request(transaction) { error in
if let error {
MXLog.error("Failed transaction with error: \(error)")
}
}
}
ongoingCallID = nil
}
} }
@@ -17,8 +17,8 @@
import Combine import Combine
enum ElementCallServiceAction { enum ElementCallServiceAction {
case answerCall(roomID: String) case startCall(roomID: String)
case declineCall(roomID: String) case endCall(roomID: String)
} }
enum ElementCallServiceNotificationKey: String { enum ElementCallServiceNotificationKey: String {
@@ -32,7 +32,7 @@ let ElementCallServiceNotificationDiscardDelta = 10.0
protocol ElementCallServiceProtocol { protocol ElementCallServiceProtocol {
var actions: AnyPublisher<ElementCallServiceAction, Never> { get } var actions: AnyPublisher<ElementCallServiceAction, Never> { get }
func setupCallSession(title: String) async func setupCallSession(roomID: String, roomDisplayName: String) async
func tearDownCallSession() func tearDownCallSession()
} }
@@ -525,7 +525,7 @@ class MockScreen: Identifiable {
appLockService: AppLockService(keychainController: KeychainControllerMock(), appLockService: AppLockService(keychainController: KeychainControllerMock(),
appSettings: ServiceLocator.shared.settings), appSettings: ServiceLocator.shared.settings),
bugReportService: BugReportServiceMock(), bugReportService: BugReportServiceMock(),
elementCallService: ElementCallServiceMock(), elementCallService: ElementCallServiceMock(.init()),
roomTimelineControllerFactory: RoomTimelineControllerFactoryMock(configuration: .init()), roomTimelineControllerFactory: RoomTimelineControllerFactoryMock(configuration: .init()),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: appSettings, appSettings: appSettings,
@@ -641,7 +641,7 @@ class MockScreen: Identifiable {
appLockService: AppLockService(keychainController: KeychainControllerMock(), appLockService: AppLockService(keychainController: KeychainControllerMock(),
appSettings: ServiceLocator.shared.settings), appSettings: ServiceLocator.shared.settings),
bugReportService: BugReportServiceMock(), bugReportService: BugReportServiceMock(),
elementCallService: ElementCallServiceMock(), elementCallService: ElementCallServiceMock(.init()),
roomTimelineControllerFactory: RoomTimelineControllerFactoryMock(configuration: .init(timelineController: timelineController)), roomTimelineControllerFactory: RoomTimelineControllerFactoryMock(configuration: .init(timelineController: timelineController)),
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: appSettings, appSettings: appSettings,
@@ -46,7 +46,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
navigationRootCoordinator: navigationRootCoordinator, navigationRootCoordinator: navigationRootCoordinator,
appLockService: AppLockServiceMock(), appLockService: AppLockServiceMock(),
bugReportService: BugReportServiceMock(), bugReportService: BugReportServiceMock(),
elementCallService: ElementCallServiceMock(), elementCallService: ElementCallServiceMock(.init()),
roomTimelineControllerFactory: timelineControllerFactory, roomTimelineControllerFactory: timelineControllerFactory,
appMediator: AppMediatorMock.default, appMediator: AppMediatorMock.default,
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,