Add support for showing the native OS incoming call screen when starting new Element Calls

This commit is contained in:
Stefan Ceriu
2024-05-17 15:13:57 +03:00
committed by Stefan Ceriu
parent 5860317fea
commit 8bfd802793
19 changed files with 460 additions and 66 deletions
@@ -0,0 +1,158 @@
//
// 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 AVFoundation
import CallKit
import Combine
import Foundation
import PushKit
class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDelegate, CXProviderDelegate {
private let pushRegistry: PKPushRegistry
private let callController = CXCallController()
private var callProvider: CXProvider?
private var ongoingCallID: UUID?
private var incomingCallRoomID: String?
private let actionsSubject: PassthroughSubject<ElementCallServiceAction, Never> = .init()
var actions: AnyPublisher<ElementCallServiceAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
override init() {
pushRegistry = PKPushRegistry(queue: nil)
super.init()
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.voIP]
}
func setupCallSession(title: String) async {
guard ongoingCallID == nil else {
return
}
let callID = UUID()
ongoingCallID = callID
let handle = CXHandle(type: .generic, value: title)
let startCallAction = CXStartCallAction(call: callID, handle: handle)
startCallAction.isVideo = true
let transaction = CXTransaction(action: startCallAction)
do {
try await callController.request(transaction)
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()
}
}
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)")
}
}
}
// MARK: - PKPushRegistryDelegate
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { }
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
guard let roomID = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomID.rawValue] as? String else {
MXLog.error("Somethnig went wrong, missing room identifier for incoming voip call: \(payload)")
return
}
let callID = UUID()
ongoingCallID = callID
incomingCallRoomID = roomID
let configuration = CXProviderConfiguration()
configuration.supportsVideo = true
configuration.includesCallsInRecents = false
let update = CXCallUpdate()
update.hasVideo = true
update.localizedCallerName = payload.dictionaryPayload[ElementCallServiceNotificationKey.roomDisplayName.rawValue] as? String
if let senderDisplayName = payload.dictionaryPayload[ElementCallServiceNotificationKey.senderDisplayName.rawValue] as? String {
update.remoteHandle = .init(type: .generic, value: senderDisplayName)
} else if let senderID = payload.dictionaryPayload[ElementCallServiceNotificationKey.senderID.rawValue] as? String {
update.remoteHandle = .init(type: .generic, value: senderID)
} else {
MXLog.error("Something went wrong, both the user display name and ID are nil")
}
let callProvider = CXProvider(configuration: configuration)
callProvider.setDelegate(self, queue: nil)
callProvider.reportNewIncomingCall(with: callID, update: update) { error in
if let error {
MXLog.error("Failed reporting new incoming call with error: \(error)")
}
completion()
}
}
// MARK: - CXProviderDelegate
func providerDidReset(_ provider: CXProvider) {
MXLog.info("Call provider did reset: \(provider)")
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
if let incomingCallRoomID {
actionsSubject.send(.answerCall(roomID: incomingCallRoomID))
self.incomingCallRoomID = nil
} else {
MXLog.error("Failed answering incoming call, missing room ID")
}
action.fulfill()
}
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")
}
action.fulfill()
}
}
@@ -0,0 +1,40 @@
//
// 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
enum ElementCallServiceAction {
case answerCall(roomID: String)
case declineCall(roomID: String)
}
enum ElementCallServiceNotificationKey: String {
case roomID
case roomDisplayName
case senderID
case senderDisplayName
}
let ElementCallServiceNotificationDiscardDelta = 10.0
// sourcery: AutoMockable
protocol ElementCallServiceProtocol {
var actions: AnyPublisher<ElementCallServiceAction, Never> { get }
func setupCallSession(title: String) async
func tearDownCallSession()
}
@@ -521,6 +521,16 @@ class RoomProxy: RoomProxyProtocol {
ElementCallWidgetDriver(room: room)
}
func sendCallNotificationIfNeeeded() async -> Result<Void, RoomProxyError> {
do {
try await room.sendCallNotificationIfNeeded()
return .success(())
} catch {
MXLog.error("Failed room call notification with error: \(error)")
return .failure(.sdkError(error))
}
}
// MARK: - Permalinks
func matrixToPermalink() async -> Result<URL, RoomProxyError> {
@@ -129,6 +129,8 @@ protocol RoomProxyProtocol {
func elementCallWidgetDriver() -> ElementCallWidgetDriverProtocol
func sendCallNotificationIfNeeeded() async -> Result<Void, RoomProxyError>
// MARK: - Permalinks
func matrixToPermalink() async -> Result<URL, RoomProxyError>