Add ProviderDelegate and TelecomManager for Callkit integration

This commit is contained in:
QuentinArguillere 2023-12-12 11:42:56 +01:00
parent 2a1bd88741
commit 3bb0d06787
6 changed files with 869 additions and 1 deletions

View file

@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
662B69D92B25DE18007118BF /* TelecomManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662B69D82B25DE18007118BF /* TelecomManager.swift */; };
662B69DB2B25DE25007118BF /* ProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662B69DA2B25DE25007118BF /* ProviderDelegate.swift */; };
66C491F92B24D25B00CEA16D /* ConfigExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491F82B24D25A00CEA16D /* ConfigExtension.swift */; };
66C491FB2B24D32600CEA16D /* CoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491FA2B24D32600CEA16D /* CoreExtension.swift */; };
66C491FD2B24D36500CEA16D /* AudioRouteUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C491FC2B24D36500CEA16D /* AudioRouteUtils.swift */; };
@ -91,6 +93,8 @@
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
662B69D82B25DE18007118BF /* TelecomManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelecomManager.swift; sourceTree = "<group>"; };
662B69DA2B25DE25007118BF /* ProviderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = "<group>"; };
66C491F82B24D25A00CEA16D /* ConfigExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigExtension.swift; sourceTree = "<group>"; };
66C491FA2B24D32600CEA16D /* CoreExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreExtension.swift; sourceTree = "<group>"; };
66C491FC2B24D36500CEA16D /* AudioRouteUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRouteUtils.swift; sourceTree = "<group>"; };
@ -188,6 +192,15 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
662B69D72B25DDF6007118BF /* TelecomManager */ = {
isa = PBXGroup;
children = (
662B69D82B25DE18007118BF /* TelecomManager.swift */,
662B69DA2B25DE25007118BF /* ProviderDelegate.swift */,
);
path = TelecomManager;
sourceTree = "<group>";
};
66C491F72B24D25A00CEA16D /* Extensions */ = {
isa = PBXGroup;
children = (
@ -247,6 +260,7 @@
D719ABB62ABC67BF00B41C10 /* LinphoneApp.swift */,
D777DBB12AE12C4000565A99 /* Contacts */,
D719ABC72ABC6FB200B41C10 /* Core */,
662B69D72B25DDF6007118BF /* TelecomManager */,
D719ABC52ABC6EE800B41C10 /* UI */,
D717071C2AC591EF0037746F /* Utils */,
D719ABBA2ABC67BF00B41C10 /* Assets.xcassets */,
@ -599,6 +613,7 @@
D7C3650C2AF0084000FE6142 /* EditContactViewModel.swift in Sources */,
D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */,
D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */,
662B69DB2B25DE25007118BF /* ProviderDelegate.swift in Sources */,
D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */,
D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */,
D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */,
@ -641,6 +656,7 @@
D7FB55112AD447FD00A5AB15 /* RegisterFragment.swift in Sources */,
D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */,
D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */,
662B69D92B25DE18007118BF /* TelecomManager.swift in Sources */,
D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */,
66C491FB2B24D32600CEA16D /* CoreExtension.swift in Sources */,
D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */,

View file

@ -149,6 +149,11 @@ final class CoreContext: ObservableObject {
cbVal.core.clearAccounts()
cbVal.core.clearAllAuthInfo()
}
TelecomManager.shared.onAccountRegistrationStateChanged(core: cbVal.core, account: cbVal.account, state: cbVal.state, message: cbVal.message)
}
self.mCore.publisher?.onCallStateChanged?.postOnCoreQueue { (cbVal: (core: Core, call: Call, state: Call.State, message: String)) in
TelecomManager.shared.onCallStateChanged(core: cbVal.core, call: cbVal.call, state: cbVal.state, message: cbVal.message)
}
self.mIteratePublisher = Timer.publish(every: 0.02, on: .main, in: .common)

View file

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Camera usage is required for video VOIP calls</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone usage is required for VOIP calls</string>
<key>UIAppFonts</key>
<array>
<string>NotoSans-Light.ttf</string>

View file

@ -0,0 +1,386 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// swiftlint:disable line_length
import Foundation
import CallKit
import UIKit
import linphonesw
import AVFoundation
import os
class CallInfo {
var callId: String = ""
var toAddr: Address?
var isOutgoing = false
var sasEnabled = false
var connected = false
var reason: Reason = Reason.None
var displayName: String?
var videoEnabled = false
var isConference = false
static func newIncomingCallInfo(callId: String) -> CallInfo {
let callInfo = CallInfo()
callInfo.callId = callId
return callInfo
}
static func newOutgoingCallInfo(addr: Address, isSas: Bool, displayName: String, isVideo: Bool, isConference: Bool) -> CallInfo {
let callInfo = CallInfo()
callInfo.isOutgoing = true
callInfo.sasEnabled = isSas
callInfo.toAddr = addr
callInfo.displayName = displayName
callInfo.videoEnabled = isVideo
callInfo.isConference = isConference
return callInfo
}
}
/*
* A delegate to support callkit.
*/
class ProviderDelegate: NSObject {
let provider: CXProvider
var uuids: [String: UUID] = [:]
var callInfos: [UUID: CallInfo] = [:]
override init() {
provider = CXProvider(configuration: ProviderDelegate.providerConfiguration)
super.init()
provider.setDelegate(self, queue: nil)
}
static var providerConfiguration: CXProviderConfiguration {
get {
let providerConfiguration = CXProviderConfiguration()
// providerConfiguration.ringtoneSound = ConfigManager.instance().lpConfigBoolForKey(key: "use_device_ringtone") ? nil : "notes_of_the_optimistic.caf"
providerConfiguration.supportsVideo = true
providerConfiguration.iconTemplateImageData = UIImage(named: "callkit_logo")?.pngData()
providerConfiguration.supportedHandleTypes = [.generic, .phoneNumber, .emailAddress]
providerConfiguration.maximumCallsPerCallGroup = 10
providerConfiguration.maximumCallGroups = 10
// not show app's calls in tel's history
// providerConfiguration.includesCallsInRecents = YES;
return providerConfiguration
}
}
func reportIncomingCall(call: Call?, uuid: UUID, handle: String, hasVideo: Bool, displayName: String) {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: handle)
update.hasVideo = hasVideo
update.localizedCallerName = displayName
let callInfo = callInfos[uuid]
let callId = callInfo?.callId ?? ""
/*
if (ConfigManager.instance().config?.hasEntry(section: "app", key: "max_calls") == 1) { // moved from misc to app section intentionally upon app start or remote configuration
if let maxCalls = ConfigManager.instance().config?.getInt(section: "app",key: "max_calls",defaultValue: 10), Core.get().callsNb > maxCalls {
Log.directLog(BCTBX_LOG_MESSAGE, text: "CallKit: declining call, as max calls (\(maxCalls)) reached call-id: [\(String(describing: callId))] and UUID: [\(uuid.description)]")
decline(uuid: uuid)
CoreContext.shared.doOnCoreQueue(synchronous: true) { core in
try? call?.decline(reason: .Busy)
}
return
}
}
*/
Log.info("CallKit: report new incoming call with call-id: [\(callId)] and UUID: [\(uuid.description)]")
// TelecomManager.instance().setHeldOtherCalls(exceptCallid: callId ?? "") // ALREADY COMMENTED ON LINPHONE-IPHONE 5.2
provider.reportNewIncomingCall(with: uuid, update: update) { error in
if error == nil {
if TelecomManager.shared.endCallkit {
CoreContext.shared.doOnCoreQueue(synchronous: true) { core in
let call = core.getCallByCallid(callId: callId)
if call?.state == .PushIncomingReceived {
try? call?.terminate()
}
}
}
} else {
Log.error("CallKit: cannot complete incoming call with call-id: [\(callId)] and UUID: [\(uuid.description)] from [\(handle)] caused by [\(error!.localizedDescription)]")
let code = (error as NSError?)?.code
switch code {
case CXErrorCodeIncomingCallError.filteredByDoNotDisturb.rawValue:
callInfo?.reason = Reason.Busy // This answer is only for this device. Using Reason.DoNotDisturb will make all other end point stop ringing.
case CXErrorCodeIncomingCallError.filteredByBlockList.rawValue:
callInfo?.reason = Reason.DoNotDisturb
default:
callInfo?.reason = Reason.Unknown
}
self.callInfos.updateValue(callInfo!, forKey: uuid)
CoreContext.shared.doOnCoreQueue(synchronous: true) { _ in
try? call?.decline(reason: callInfo!.reason)
}
}
}
}
func updateCall(uuid: UUID, handle: String, hasVideo: Bool = false, displayName: String) {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: handle)
update.localizedCallerName = displayName
update.hasVideo = hasVideo
provider.reportCall(with: uuid, updated: update)
}
func reportOutgoingCallStartedConnecting(uuid: UUID) {
provider.reportOutgoingCall(with: uuid, startedConnectingAt: nil)
}
func reportOutgoingCallConnected(uuid: UUID) {
provider.reportOutgoingCall(with: uuid, connectedAt: nil)
}
func endCall(uuid: UUID) {
provider.reportCall(with: uuid, endedAt: .init(), reason: .failed)
}
func decline(uuid: UUID) {
provider.reportCall(with: uuid, endedAt: .init(), reason: .unanswered)
}
func endCallNotExist(uuid: UUID, timeout: DispatchTime) {
DispatchQueue.main.asyncAfter(deadline: timeout) {
CoreContext.shared.doOnCoreQueue(synchronous: true) { core in
let callId = TelecomManager.shared.providerDelegate.callInfos[uuid]?.callId
if callId == nil {
// callkit already ended
return
}
if core.getCallByCallid(callId: callId ?? "") == nil {
Log.info("CallKit: terminate call with call-id: \(String(describing: callId)) and UUID: \(uuid) which does not exist.")
self.endCall(uuid: uuid)
}
}
}
}
}
// MARK: - CXProviderDelegate
extension ProviderDelegate: CXProviderDelegate {
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
let uuid = action.callUUID
let callId = callInfos[uuid]?.callId
// remove call infos first, otherwise CXEndCallAction will be called more than onece
if callId != nil {
uuids.removeValue(forKey: callId!)
}
callInfos.removeValue(forKey: uuid)
CoreContext.shared.doOnCoreQueue { core in
if let call = core.getCallByCallid(callId: callId ?? "") {
TelecomManager.shared.terminateCall(call: call)
Log.info("CallKit: Call ended with call-id: \(String(describing: callId)) an UUID: \(uuid.description).")
}
action.fulfill()
}
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
let uuid = action.callUUID
let callInfo = callInfos[uuid]
let callId = callInfo?.callId ?? ""
CoreContext.shared.doOnCoreQueue { core in
Log.info("CallKit: answer call with call-id: \(String(describing: callId)) and UUID: \(uuid.description).")
let call = core.getCallByCallid(callId: callId)
if UIApplication.shared.applicationState != .active {
TelecomManager.shared.backgroundContextCall = call
TelecomManager.shared.backgroundContextCameraIsEnabled = call?.params?.videoEnabled == true || call?.callLog?.wasConference() == true
if #available(iOS 16.0, *) {
if call?.cameraEnabled == true {
call?.cameraEnabled = AVCaptureSession().isMultitaskingCameraAccessSupported
}
} else {
call?.cameraEnabled = false // Disable camera while app is not on foreground
}
}
TelecomManager.shared.callkitAudioSessionActivated = false
core.configureAudioSession()
TelecomManager.shared.acceptCall(core: core, call: call!, hasVideo: call!.params?.videoEnabled ?? false)
action.fulfill()
}
}
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
let uuid = action.callUUID
let callId = callInfos[uuid]?.callId ?? ""
CoreContext.shared.doOnCoreQueue { core in
let call = core.getCallByCallid(callId: callId)
if call == nil {
Log.error("CXSetHeldCallAction: no call !")
action.fail()
return
}
do {
if call?.conference != nil && action.isOnHold {
_ = call?.conference?.leave()
Log.info("CallKit: call-id: [\(callId)] leaving conference")
NotificationCenter.default.post(name: Notification.Name("LinphoneCallUpdate"), object: self)
action.fulfill()
} else {
let state = action.isOnHold ? "Paused" : "Resumed"
Log.info("CallKit: Call with call-id: [\(callId)] and UUID: [\(uuid)] paused status changed to: [\(state)]")
if action.isOnHold {
TelecomManager.shared.speakerBeforePause = AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed(core: core, call: call)
try call!.pause()
// fullfill() the action now to indicate to Callkit that this call is no longer active, even if the
// SIP transaction is not completed yet. At this stage, the media streams are off.
// If callkit is not aware that the pause action is completed, it will terminate this call if we
// attempt to resume another one.
action.fulfill()
} else {
if call?.conference != nil && core.callsNb ?? 0 > 1 {/*
try TelecomManager.shared.lc?.enterConference()
action.fulfill()
NotificationCenter.default.post(name: Notification.Name("LinphoneCallUpdate"), object: self)
*/} else {
try call!.resume()
// We'll notify callkit that the action is fulfilled when receiving the 200Ok, which is the point
// where we actually start the media streams.
TelecomManager.shared.actionToFulFill = action
// HORRIBLE HACK HERE - PLEASE APPLE FIX THIS !!
// When resuming a SIP call after a native call has ended remotely, didActivate: audioSession
// is never called.
// It looks like in this case, it is implicit.
// As a result we have to notify the Core that the AudioSession is active.
// The SpeakerBox demo application written by Apple exhibits this behavior.
// https://developer.apple.com/documentation/callkit/making_and_receiving_voip_calls_with_callkit
// We can clearly see there that startAudio() is called immediately in the CXSetHeldCallAction
// handler, while it is called from didActivate: audioSession otherwise.
// Callkit's design is not consistent, or its documentation imcomplete, wich is somewhat disapointing.
//
Log.info("Assuming AudioSession is active when executing a CXSetHeldCallAction with isOnHold=false.")
core.activateAudioSession(actived: true)
TelecomManager.shared.callkitAudioSessionActivated = true
}
}
}
} catch {
Log.error("CallKit: Call set held (paused or resumed) \(uuid) failed because \(error)")
action.fail()
}
}
}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
let uuid = action.callUUID
let callInfo = callInfos[uuid]
let update = CXCallUpdate()
update.remoteHandle = action.handle
update.localizedCallerName = callInfo?.displayName
self.provider.reportCall(with: action.callUUID, updated: update)
let addr = callInfo?.toAddr
if addr == nil {
Log.info("CallKit: can not call a null address!")
action.fail()
} else {
CoreContext.shared.doOnCoreQueue { core in
do {
core.configureAudioSession()
try TelecomManager.shared.doCall(core: core, addr: addr!, isSas: callInfo?.sasEnabled ?? false, isVideo: callInfo?.videoEnabled ?? false, isConference: callInfo?.isConference ?? false)
action.fulfill()
} catch {
Log.info("CallKit: Call started failed because \(error)")
action.fail()
}
}
}
}
func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) {
CoreContext.shared.doOnCoreQueue { core in
Log.info("CallKit: Call grouped callUUid : \(action.callUUID) with callUUID: \(String(describing: action.callUUIDToGroupWith)).")
TelecomManager.shared.addAllToLocalConference(core: core)
action.fulfill()
}
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
let uuid = action.callUUID
let callId = callInfos[uuid]?.callId
CoreContext.shared.doOnCoreQueue { core in
Log.info( "CallKit: Call muted with call-id: \(String(describing: callId)) an UUID: \(uuid.description).")
core.micEnabled = !core.micEnabled
action.fulfill()
}
}
func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
let uuid = action.callUUID
let callId = callInfos[uuid]?.callId ?? ""
CoreContext.shared.doOnCoreQueue { core in
Log.info("CallKit: Call send dtmf with call-id: \(callId) an UUID: \(uuid.description).")
if let call = core.getCallByCallid(callId: callId) {
let digit = (action.digits.cString(using: String.Encoding.utf8)?[0])!
do {
try call.sendDtmf(dtmf: digit)
} catch {
Log.error("CallKit: Call send dtmf \(uuid) failed because \(error)")
}
}
action.fulfill()
}
}
func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
let uuid = action.uuid
let callId = callInfos[uuid]?.callId
Log.error("CallKit: Call time out with call-id: \(String(describing: callId)) an UUID: \(uuid.description).")
action.fulfill()
}
func providerDidReset(_ provider: CXProvider) {
Log.info("CallKit: did reset.")
}
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
CoreContext.shared.doOnCoreQueue { core in
Log.info("CallKit: audio session activated.")
core.activateAudioSession(actived: true)
TelecomManager.shared.callkitAudioSessionActivated = true
}
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
CoreContext.shared.doOnCoreQueue { core in
Log.info("CallKit: audio session deactivated.")
core.activateAudioSession(actived: false)
TelecomManager.shared.callkitAudioSessionActivated = nil
}
}
}
// swiftlint:enable line_length

View file

@ -0,0 +1,456 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-iphone
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// swiftlint:disable cyclomatic_complexity
import Foundation
import linphonesw
import UserNotifications
import os
import CallKit
import AVFoundation
class CallAppData: NSObject {
var batteryWarningShown = false
var videoRequested = false /*set when user has requested for video*/
var isConference = false
}
class TelecomManager {
static let shared = TelecomManager()
static var uuidReplacedCall: String?
let providerDelegate: ProviderDelegate // to support callkit
let callController: CXCallController // to support callkit
var actionToFulFill: CXCallAction?
var callkitAudioSessionActivated: Bool?
var nextCallIsTransfer: Bool = false
var speakerBeforePause: Bool = false
var endCallkit: Bool = false
var endCallKitReplacedCall: Bool = true
var backgroundContextCall: Call?
var backgroundContextCameraIsEnabled: Bool = false
var referedFromCall: String?
var referedToCall: String?
var actionsToPerformOnceWhenCoreIsOn: [(() -> Void)] = []
private init() {
providerDelegate = ProviderDelegate()
callController = CXCallController()
}
func addAllToLocalConference(core: Core) {
// TODO
}
static func getAppData(sCall: Call) -> CallAppData? {
if sCall.userData == nil {
return nil
}
return Unmanaged<CallAppData>.fromOpaque(sCall.userData!).takeUnretainedValue()
}
static func setAppData(sCall: Call, appData: CallAppData?) {
if sCall.userData != nil {
Unmanaged<CallAppData>.fromOpaque(sCall.userData!).release()
}
if appData == nil {
sCall.userData = nil
} else {
sCall.userData = UnsafeMutableRawPointer(Unmanaged.passRetained(appData!).toOpaque())
}
}
func doCall(core: Core, addr: Address, isSas: Bool, isVideo: Bool, isConference: Bool = false) throws {
// let displayName = FastAddressBook.displayName(for: addr.getCobject)
let lcallParams = try core.createCallParams(call: nil)
/*
if ConfigManager.instance().lpConfigBoolForKey(key: "edge_opt_preference") && AppManager.network() == .network_2g {
Log.directLog(BCTBX_LOG_MESSAGE, text: "Enabling low bandwidth mode")
lcallParams.lowBandwidthEnabled = true
}
if (displayName != nil) {
try addr.setDisplayname(newValue: displayName!)
}
if(ConfigManager.instance().lpConfigBoolForKey(key: "override_domain_with_default_one")) {
try addr.setDomain(newValue: ConfigManager.instance().lpConfigStringForKey(key: "domain", section: "assistant"))
}
*/
if nextCallIsTransfer {
let call = core.currentCall
try call?.transferTo(referTo: addr)
nextCallIsTransfer = false
} else {
// We set the record file name here because we can't do it after the call is started.
// let writablePath = AppManager.recordingFilePathFromCall(address: addr.username! )
// Log.directLog(BCTBX_LOG_DEBUG, text: "record file path: \(writablePath)")
// lcallParams.recordFile = writablePath
if isSas {
lcallParams.mediaEncryption = .ZRTP
}
if isConference {
/* if (ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! != .AudioOnly) {
lcallParams.videoEnabled = true
lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly
lcallParams.conferenceVideoLayout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! == .Grid ? .Grid : .ActiveSpeaker
} else {
lcallParams.videoEnabled = false
}*/
} else {
lcallParams.videoEnabled = isVideo
}
if let call = core.inviteAddressWithParams(addr: addr, params: lcallParams) {
// The LinphoneCallAppData object should be set on call creation with callback
// - (void)onCall:StateChanged:withMessage:. If not, we are in big trouble and expect it to crash
// We are NOT responsible for creating the AppData.
if let data = TelecomManager.getAppData(sCall: call) {
data.isConference = isConference
data.videoRequested = lcallParams.videoEnabled
TelecomManager.setAppData(sCall: call, appData: data)
} else {
Log.error("New call instanciated but app data was not set. Expect it to crash.")
/* will be used later to notify user if video was not activated because of the linphone core*/
}
}
}
}
func acceptCall(core: Core, call: Call, hasVideo: Bool) {
do {
let callParams = try core.createCallParams(call: call)
callParams.videoEnabled = hasVideo
/*if (ConfigManager.instance().lpConfigBoolForKey(key: "edge_opt_preference")) {
let low_bandwidth = (AppManager.network() == .network_2g)
if (low_bandwidth) {
Log.directLog(BCTBX_LOG_MESSAGE, text: "Low bandwidth mode")
}
callParams.lowBandwidthEnabled = low_bandwidth
}*/
// We set the record file name here because we can't do it after the call is started.
// let address = call.callLog?.fromAddress
// let writablePath = AppManager.recordingFilePathFromCall(address: address?.username ?? "")
// Log.directLog(BCTBX_LOG_MESSAGE, text: "Record file path: \(String(describing: writablePath))")
// callParams.recordFile = writablePath
/*
if let chatView : ChatConversationView = PhoneMainView.instance().VIEW(ChatConversationView.compositeViewDescription()), chatView.isVoiceRecording {
Log.directLog(BCTBX_LOG_MESSAGE, text: "Voice recording in progress, stopping it befoce accepting the call.")
chatView.stopVoiceRecording()
}*/
if call.callLog?.wasConference() == true {
// Prevent incoming group call to start in audio only layout
// Do the same as the conference waiting room
callParams.videoEnabled = true
callParams.videoDirection = core.videoActivationPolicy?.automaticallyInitiate == true ? .SendRecv : .RecvOnly
Log.info("[Context] Enabling video on call params to prevent audio-only layout when answering")
}
try call.acceptWithParams(params: callParams)
} catch {
Log.error("accept call failed \(error)")
}
}
func terminateCall(call: Call) {
do {
try call.terminate()
Log.info("Call terminated")
} catch {
Log.error("Failed to terminate call failed because \(error)")
}
}
func displayIncomingCall(call: Call?, handle: String, hasVideo: Bool, callId: String, displayName: String) {
let uuid = UUID()
let callInfo = CallInfo.newIncomingCallInfo(callId: callId)
providerDelegate.callInfos.updateValue(callInfo, forKey: uuid)
providerDelegate.uuids.updateValue(uuid, forKey: callId)
providerDelegate.reportIncomingCall(call: call, uuid: uuid, handle: handle, hasVideo: hasVideo, displayName: displayName)
}
func incomingDisplayName(call: Call) -> String {
// TODO
return "IncomingDisplayName"
}
static func callKitEnabled(core: Core) -> Bool {
#if !targetEnvironment(simulator)
return core.callkitEnabled
#endif
return false
}
func requestTransaction(_ transaction: CXTransaction, action: String) {
callController.request(transaction) { error in
if let error = error {
Log.error("CallKit: Requested transaction \(action) failed because: \(error)")
} else {
Log.info("CallKit: Requested transaction \(action) successfully")
}
}
}
func onAccountRegistrationStateChanged(core: Core, account: Account, state: RegistrationState, message: String) {
if core.accountList.count == 1 && (state == .Failed || state == .Cleared) {
// terminate callkit immediately when registration failed or cleared, supporting single account configuration
for call in providerDelegate.uuids {
let callId = providerDelegate.callInfos[call.value]?.callId
if callId != nil {
let call = core.getCallByCallid(callId: callId!)
if call != nil && call?.state != .PushIncomingReceived {
// sometimes (for example) due to network, registration failed, in this case, keep the call
continue
}
}
providerDelegate.endCall(uuid: call.value)
}
endCallkit = true
} else {
endCallkit = false
}
}
func onCallStateChanged(core: Core, call: Call, state cstate: Call.State, message: String) {
let callLog = call.callLog
let callId = callLog?.callId ?? ""
if cstate == .PushIncomingReceived {
displayIncomingCall(call: call, handle: "Calling", hasVideo: false, callId: callId, displayName: "Calling")
} else {
let video = (core.videoActivationPolicy?.automaticallyAccept ?? false) && (call.remoteParams?.videoEnabled ?? false)
if call.userData == nil {
let appData = CallAppData()
TelecomManager.setAppData(sCall: call, appData: appData)
}
/*
if let conference = call.conference, ConferenceViewModel.shared.conference.value == nil {
Log.info("[Call] Found conference attached to call and no conference in dedicated view model, init & configure it")
ConferenceViewModel.shared.initConference(conference)
ConferenceViewModel.shared.configureConference(conference)
}
*/
switch cstate {
case .IncomingReceived:
let addr = call.remoteAddress
let displayName = incomingDisplayName(call: call)
if call.replacedCall != nil {
endCallKitReplacedCall = false
let uuid = providerDelegate.uuids["\(TelecomManager.uuidReplacedCall ?? "")"]
let callInfo = providerDelegate.callInfos[uuid!]
callInfo!.callId = referedToCall ?? ""
if callInfo != nil && uuid != nil && addr != nil {
providerDelegate.callInfos.updateValue(callInfo!, forKey: uuid!)
providerDelegate.uuids.removeValue(forKey: callId)
providerDelegate.uuids.updateValue(uuid!, forKey: callInfo!.callId)
providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: video, displayName: displayName)
}
} else if TelecomManager.callKitEnabled(core: core) {
/*
let isConference = isConferenceCall(call: call)
let isEarlyConference = isConference && CallsViewModel.shared.currentCallData.value??.isConferenceCall.value != true // Conference info not be received yet.
if (isEarlyConference) {
CallsViewModel.shared.currentCallData.readCurrentAndObserve { _ in
let uuid = providerDelegate.uuids["\(callId)"]
if (uuid != nil) {
displayName = "\(VoipTexts.conference_incoming_title): \(CallsViewModel.shared.currentCallData.value??.remoteConferenceSubject.value ?? "") (\(CallsViewModel.shared.currentCallData.value??.conferenceParticipantsCountLabel.value ?? ""))"
providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: video, displayName: displayName)
}
}
}
*/
let uuid = providerDelegate.uuids["\(callId)"]
if call.replacedCall == nil {
TelecomManager.uuidReplacedCall = callId
}
if uuid != nil {
// Tha app is now registered, updated the call already existed.
providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: video, displayName: displayName)
} else {
displayIncomingCall(call: call, handle: addr!.asStringUriOnly(), hasVideo: video, callId: callId, displayName: displayName)
}
} /* else if UIApplication.shared.applicationState != .active {
// not support callkit , use notif
let content = UNMutableNotificationContent()
content.title = NSLocalizedString("Incoming call", comment: "")
content.body = displayName
content.sound = UNNotificationSound.init(named: UNNotificationSoundName.init("notes_of_the_optimistic.caf"))
content.categoryIdentifier = "call_cat"
content.userInfo = ["CallId": callId]
let req = UNNotificationRequest.init(identifier: "call_request", content: content, trigger: nil)
UNUserNotificationCenter.current().add(req, withCompletionHandler: nil)
} */
case .StreamsRunning:
if TelecomManager.callKitEnabled(core: core) {
let uuid = providerDelegate.uuids["\(callId)"]
if uuid != nil {
let callInfo = providerDelegate.callInfos[uuid!]
if callInfo != nil && callInfo!.isOutgoing && !callInfo!.connected {
Log.info("CallKit: outgoing call connected with uuid \(uuid!) and callId \(callId)")
providerDelegate.reportOutgoingCallConnected(uuid: uuid!)
callInfo!.connected = true
providerDelegate.callInfos.updateValue(callInfo!, forKey: uuid!)
}
}
}
if speakerBeforePause {
speakerBeforePause = false
AudioRouteUtils.routeAudioToSpeaker(core: core)
}
actionToFulFill?.fulfill()
actionToFulFill = nil
case .Paused:
actionToFulFill?.fulfill()
actionToFulFill = nil
case .OutgoingInit,
.OutgoingProgress,
.OutgoingRinging,
.OutgoingEarlyMedia:
if TelecomManager.callKitEnabled(core: core) {
let uuid = providerDelegate.uuids[""]
if uuid != nil && callId.isEmpty {
let callInfo = providerDelegate.callInfos[uuid!]
callInfo!.callId = callId
providerDelegate.callInfos.updateValue(callInfo!, forKey: uuid!)
providerDelegate.uuids.removeValue(forKey: "")
providerDelegate.uuids.updateValue(uuid!, forKey: callId)
Log.info("CallKit: outgoing call started connecting with uuid \(uuid!) and callId \(callId)")
providerDelegate.reportOutgoingCallStartedConnecting(uuid: uuid!)
} else {
if false { /* isConferenceCall(call: call) {
let uuid = UUID()
let callInfo = CallInfo.newOutgoingCallInfo(addr: call.remoteAddress!, isSas: call.params?.mediaEncryption == .ZRTP, displayName: VoipTexts.conference_default_title, isVideo: call.params?.videoEnabled == true, isConference:true)
providerDelegate.callInfos.updateValue(callInfo, forKey: uuid)
providerDelegate.uuids.updateValue(uuid, forKey: "")
providerDelegate.reportOutgoingCallStartedConnecting(uuid: uuid)
Core.get().activateAudioSession(actived: true) */
} else {
referedToCall = callId
}
}
}
case .End,
.Error:
var displayName = "Unknown"
if call.dir == .Incoming {
displayName = incomingDisplayName(call: call)
} else { // if let addr = call.remoteAddress, let contactName = FastAddressBook.displayName(for: addr.getCobject) {
displayName = "TODOContactName"
}
UIDevice.current.isProximityMonitoringEnabled = false
if core.callsNb == 0 {
core.outputAudioDevice = core.defaultOutputAudioDevice
// disable this because I don't find anygood reason for it: _bluetoothAvailable = FALSE;
// furthermore it introduces a bug when calling multiple times since route may not be
// reconfigured between cause leading to bluetooth being disabled while it should not
// bluetoothEnabled = false
}
if UIApplication.shared.applicationState != .active && (callLog == nil || callLog?.status == .Missed || callLog?.status == .Aborted || callLog?.status == .EarlyAborted) {
// Configure the notification's payload.
let content = UNMutableNotificationContent()
content.title = NSString.localizedUserNotificationString(forKey: NSLocalizedString("Missed call", comment: ""), arguments: nil)
content.body = NSString.localizedUserNotificationString(forKey: displayName, arguments: nil)
// Deliver the notification.
let request = UNNotificationRequest(identifier: "call_request", content: content, trigger: nil) // Schedule the notification.
let center = UNUserNotificationCenter.current()
center.add(request) { (error: Error?) in
if error != nil {
Log.info("Error while adding notification request : \(error!.localizedDescription)")
}
}
}
if TelecomManager.callKitEnabled(core: core) {
var uuid = providerDelegate.uuids["\(callId)"]
if callId == referedToCall {
// refered call ended before connecting
Log.info("Callkit: end refered to call: \(String(describing: referedToCall))")
referedFromCall = nil
referedToCall = nil
}
if uuid == nil {
// the call not yet connected
uuid = providerDelegate.uuids[""]
}
if uuid != nil {
if callId == referedFromCall {
Log.info("Callkit: end refered from call: \(String(describing: referedFromCall))")
referedFromCall = nil
let callInfo = providerDelegate.callInfos[uuid!]
callInfo!.callId = referedToCall ?? ""
providerDelegate.callInfos.updateValue(callInfo!, forKey: uuid!)
providerDelegate.uuids.removeValue(forKey: callId)
providerDelegate.uuids.updateValue(uuid!, forKey: callInfo!.callId)
referedToCall = nil
break
}
if endCallKitReplacedCall {
let transaction = CXTransaction(action: CXEndCallAction(call: uuid!))
requestTransaction(transaction, action: "endCall")
} else {
endCallKitReplacedCall = true
}
}
}
case .Released:
TelecomManager.setAppData(sCall: call, appData: nil)
case .Referred:
referedFromCall = call.callLog?.callId
default:
break
}
let readyForRoutechange = callkitAudioSessionActivated == nil || (callkitAudioSessionActivated == true)
if readyForRoutechange && (cstate == .IncomingReceived || cstate == .OutgoingInit || cstate == .Connected || cstate == .StreamsRunning) {
if (call.currentParams?.videoEnabled ?? false) && AudioRouteUtils.isReceiverEnabled(core: core) && call.conference == nil {
AudioRouteUtils.routeAudioToSpeaker(core: core, call: call)
} else if AudioRouteUtils.isBluetoothAvailable(core: core) {
// Use bluetooth device by default if one is available
AudioRouteUtils.routeAudioToBluetooth(core: core, call: call)
}
}
}
// post Notification kLinphoneCallUpdate
NotificationCenter.default.post(name: Notification.Name("LinphoneCallUpdate"), object: self, userInfo: [
AnyHashable("call"): NSValue.init(pointer: UnsafeRawPointer(call.getCobject)),
AnyHashable("state"): NSNumber(value: cstate.rawValue),
AnyHashable("message"): message
])
}
}
// swiftlint:enable cyclomatic_complexity

View file

@ -49,7 +49,7 @@ class HistoryListViewModel: ObservableObject {
self.callLogsTmp.append(log)
}
}
/*
DispatchQueue.main.async {
self.coreDelegate = CoreDelegateStub(
onCallLogUpdated: { (_: Core, _: CallLog) -> Void in
@ -71,6 +71,7 @@ class HistoryListViewModel: ObservableObject {
core.addDelegate(delegate: self.coreDelegate!)
}
}
*/
}
}