mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +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:
@@ -19,19 +19,36 @@ import CallKit
|
||||
import Combine
|
||||
import Foundation
|
||||
import PushKit
|
||||
import UIKit
|
||||
|
||||
class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDelegate, CXProviderDelegate {
|
||||
private struct CallID: Equatable {
|
||||
let callKitID: UUID
|
||||
let roomID: String
|
||||
}
|
||||
|
||||
private let pushRegistry: PKPushRegistry
|
||||
|
||||
private let callController = CXCallController()
|
||||
|
||||
private var callProvider: CXProvider?
|
||||
private var ongoingCallID: UUID?
|
||||
|
||||
private var incomingCallRoomID: String?
|
||||
private let callProvider: CXProvider = {
|
||||
let configuration = CXProviderConfiguration()
|
||||
configuration.supportsVideo = true
|
||||
configuration.includesCallsInRecents = true
|
||||
|
||||
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 ongoingCallID: CallID?
|
||||
|
||||
private let actionsSubject: PassthroughSubject<ElementCallServiceAction, Never> = .init()
|
||||
var actions: AnyPublisher<ElementCallServiceAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
@@ -44,47 +61,47 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
|
||||
pushRegistry.delegate = self
|
||||
pushRegistry.desiredPushTypes = [.voIP]
|
||||
|
||||
callProvider.setDelegate(self, queue: nil)
|
||||
}
|
||||
|
||||
func setupCallSession(title: String) async {
|
||||
guard ongoingCallID == nil else {
|
||||
return
|
||||
func setupCallSession(roomID: String, roomDisplayName: String) async {
|
||||
// Drop any ongoing calls when starting a new one
|
||||
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
|
||||
|
||||
let handle = CXHandle(type: .generic, value: title)
|
||||
let startCallAction = CXStartCallAction(call: callID, handle: handle)
|
||||
let handle = CXHandle(type: .generic, value: roomDisplayName)
|
||||
let startCallAction = CXStartCallAction(call: callID.callKitID, handle: handle)
|
||||
startCallAction.isVideo = true
|
||||
|
||||
let transaction = CXTransaction(action: startCallAction)
|
||||
|
||||
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().setActive(true, options: .notifyOthersOnDeactivation)
|
||||
} catch {
|
||||
MXLog.error("Failed setting up VoIP session with error: \(error)")
|
||||
tearDownCallSession()
|
||||
MXLog.error("Failed setting up audio session with error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func tearDownCallSession() {
|
||||
guard let ongoingCallID else {
|
||||
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)")
|
||||
}
|
||||
}
|
||||
tearDownCallSession(sendEndCallAction: true)
|
||||
}
|
||||
|
||||
// MARK: - PKPushRegistryDelegate
|
||||
@@ -97,31 +114,18 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
return
|
||||
}
|
||||
|
||||
let callID = UUID()
|
||||
ongoingCallID = callID
|
||||
let callID = CallID(callKitID: UUID(), roomID: roomID)
|
||||
incomingCallID = callID
|
||||
|
||||
incomingCallRoomID = roomID
|
||||
|
||||
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 roomDisplayName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String
|
||||
|
||||
let update = CXCallUpdate()
|
||||
update.hasVideo = true
|
||||
|
||||
update.localizedCallerName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String
|
||||
|
||||
update.localizedCallerName = roomDisplayName
|
||||
// https://stackoverflow.com/a/41230020/730924
|
||||
update.remoteHandle = .init(type: .generic, value: roomID)
|
||||
|
||||
let callProvider = CXProvider(configuration: configuration)
|
||||
callProvider.setDelegate(self, queue: nil)
|
||||
callProvider.reportNewIncomingCall(with: callID, update: update) { error in
|
||||
callProvider.reportNewIncomingCall(with: callID.callKitID, update: update) { error in
|
||||
if let error {
|
||||
MXLog.error("Failed reporting new incoming call with error: \(error)")
|
||||
}
|
||||
@@ -129,14 +133,15 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
|
||||
completion()
|
||||
}
|
||||
|
||||
endUnansweredCallTask = Task { [weak self, callProvider, callID] in
|
||||
endUnansweredCallTask = Task { [weak self] in
|
||||
try? await Task.sleep(for: .seconds(15))
|
||||
guard !Task.isCancelled else {
|
||||
|
||||
guard let self, !Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
|
||||
if let ongoingCallID = self?.ongoingCallID, ongoingCallID == callID {
|
||||
callProvider.reportCall(with: callID, endedAt: .now, reason: .unanswered)
|
||||
if let incomingCallID, incomingCallID.callKitID == callID.callKitID {
|
||||
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)")
|
||||
}
|
||||
|
||||
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) {
|
||||
if let incomingCallRoomID {
|
||||
Task {
|
||||
// Dispatch to next run loop so it doesn't conflict with `setupCallSession`
|
||||
actionsSubject.send(.answerCall(roomID: incomingCallRoomID))
|
||||
}
|
||||
self.incomingCallRoomID = nil
|
||||
if let incomingCallID {
|
||||
actionsSubject.send(.startCall(roomID: incomingCallID.roomID))
|
||||
endUnansweredCallTask?.cancel()
|
||||
} else {
|
||||
MXLog.error("Failed answering incoming call, missing room ID")
|
||||
MXLog.error("Failed answering incoming call, missing incomingCallID")
|
||||
}
|
||||
|
||||
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) {
|
||||
if let incomingCallRoomID {
|
||||
actionsSubject.send(.declineCall(roomID: incomingCallRoomID))
|
||||
self.incomingCallRoomID = nil
|
||||
} else {
|
||||
MXLog.error("Failed declining incoming call, missing room ID")
|
||||
if let ongoingCallID {
|
||||
actionsSubject.send(.endCall(roomID: ongoingCallID.roomID))
|
||||
}
|
||||
|
||||
|
||||
tearDownCallSession(sendEndCallAction: false)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user