diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index e7976994a..901406bbe 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -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 = ""; }; + 662B69DA2B25DE25007118BF /* ProviderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = ""; }; 66C491F82B24D25A00CEA16D /* ConfigExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigExtension.swift; sourceTree = ""; }; 66C491FA2B24D32600CEA16D /* CoreExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreExtension.swift; sourceTree = ""; }; 66C491FC2B24D36500CEA16D /* AudioRouteUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRouteUtils.swift; sourceTree = ""; }; @@ -188,6 +192,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 662B69D72B25DDF6007118BF /* TelecomManager */ = { + isa = PBXGroup; + children = ( + 662B69D82B25DE18007118BF /* TelecomManager.swift */, + 662B69DA2B25DE25007118BF /* ProviderDelegate.swift */, + ); + path = TelecomManager; + sourceTree = ""; + }; 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 */, diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index ce607a4da..f2592ae60 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -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) diff --git a/Linphone/Info.plist b/Linphone/Info.plist index 5aaaf7fac..340c36eb2 100644 --- a/Linphone/Info.plist +++ b/Linphone/Info.plist @@ -2,6 +2,10 @@ + NSCameraUsageDescription + Camera usage is required for video VOIP calls + NSMicrophoneUsageDescription + Microphone usage is required for VOIP calls UIAppFonts NotoSans-Light.ttf diff --git a/Linphone/TelecomManager/ProviderDelegate.swift b/Linphone/TelecomManager/ProviderDelegate.swift new file mode 100644 index 000000000..348512a04 --- /dev/null +++ b/Linphone/TelecomManager/ProviderDelegate.swift @@ -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 . +*/ +// 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 diff --git a/Linphone/TelecomManager/TelecomManager.swift b/Linphone/TelecomManager/TelecomManager.swift new file mode 100644 index 000000000..b92ae1eca --- /dev/null +++ b/Linphone/TelecomManager/TelecomManager.swift @@ -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 . + */ +// 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.fromOpaque(sCall.userData!).takeUnretainedValue() + } + static func setAppData(sCall: Call, appData: CallAppData?) { + if sCall.userData != nil { + Unmanaged.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 diff --git a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift index 19e7c9668..a16ea380e 100644 --- a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift +++ b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift @@ -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!) } } + */ } }