mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
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:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user