import Foundation import UIKit import CallKit import Intents import AVFoundation import Postbox import TelegramCore import SwiftSignalKit import AppBundle import AccountContext import TelegramAudio import TelegramVoip private let sharedProviderDelegate: AnyObject? = { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { return CallKitProviderDelegate() } else { return nil } }() public final class CallKitIntegration { public static var isAvailable: Bool { #if targetEnvironment(simulator) return false #else if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { return Locale.current.regionCode?.lowercased() != "cn" } else { return false } #endif } private let audioSessionActivePromise = ValuePromise(false, ignoreRepeated: true) var audioSessionActive: Signal { return self.audioSessionActivePromise.get() } private static let sharedInstance: CallKitIntegration? = CallKitIntegration() public static var shared: CallKitIntegration? { return self.sharedInstance } func setup( startCall: @escaping (AccountContext, UUID, EnginePeer.Id?, String, Bool) -> Signal, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal, setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void ) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { (sharedProviderDelegate as? CallKitProviderDelegate)?.setup(audioSessionActivePromise: self.audioSessionActivePromise, startCall: startCall, answerCall: answerCall, endCall: endCall, setCallMuted: setCallMuted, audioSessionActivationChanged: audioSessionActivationChanged) } } private init?() { if !CallKitIntegration.isAvailable { return nil } if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { } else { return nil } } func startCall(context: AccountContext, peerId: PeerId, phoneNumber: String?, localContactId: String?, isVideo: Bool, displayTitle: String) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { (sharedProviderDelegate as? CallKitProviderDelegate)?.startCall(context: context, peerId: peerId, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle) self.donateIntent(peerId: peerId, displayTitle: displayTitle, localContactId: localContactId) } } func answerCall(uuid: UUID) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { (sharedProviderDelegate as? CallKitProviderDelegate)?.answerCall(uuid: uuid) } } public func dropCall(uuid: UUID) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { (sharedProviderDelegate as? CallKitProviderDelegate)?.dropCall(uuid: uuid) } } public func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, phoneNumber: String?, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { (sharedProviderDelegate as? CallKitProviderDelegate)?.reportIncomingCall(uuid: uuid, stableId: stableId, handle: handle, phoneNumber: phoneNumber, isVideo: isVideo, displayTitle: displayTitle, completion: completion) } } func reportOutgoingCallConnected(uuid: UUID, at date: Date) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { (sharedProviderDelegate as? CallKitProviderDelegate)?.reportOutgoingCallConnected(uuid: uuid, at: date) } } private func donateIntent(peerId: PeerId, displayTitle: String, localContactId: String?) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { let handle = INPersonHandle(value: "tg\(peerId.id._internalGetInt64Value())", type: .unknown) let contact = INPerson(personHandle: handle, nameComponents: nil, displayName: displayTitle, image: nil, contactIdentifier: localContactId, customIdentifier: "tg\(peerId.id._internalGetInt64Value())") let intent = INStartAudioCallIntent(destinationType: .normal, contacts: [contact]) let interaction = INInteraction(intent: intent, response: nil) interaction.direction = .outgoing interaction.donate { _ in } } } public func applyVoiceChatOutputMode(outputMode: AudioSessionOutputMode) { (sharedProviderDelegate as? CallKitProviderDelegate)?.applyVoiceChatOutputMode(outputMode: outputMode) } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) class CallKitProviderDelegate: NSObject, CXProviderDelegate { private let provider: CXProvider private let callController = CXCallController() private var currentStartCallAccount: (UUID, AccountContext)? private var alreadyReportedIncomingCalls = Set() private var uuidToPeerIdMapping: [UUID: EnginePeer.Id] = [:] private var startCall: ((AccountContext, UUID, EnginePeer.Id?, String, Bool) -> Signal)? private var answerCall: ((UUID) -> Void)? private var endCall: ((UUID) -> Signal)? private var setCallMuted: ((UUID, Bool) -> Void)? private var audioSessionActivationChanged: ((Bool) -> Void)? private var isAudioSessionActive: Bool = false private var pendingVoiceChatOutputMode: AudioSessionOutputMode? private let disposableSet = DisposableSet() fileprivate var audioSessionActivePromise: ValuePromise? override init() { self.provider = CXProvider(configuration: CallKitProviderDelegate.providerConfiguration()) super.init() self.provider.setDelegate(self, queue: nil) } func setup(audioSessionActivePromise: ValuePromise, startCall: @escaping (AccountContext, UUID, EnginePeer.Id?, String, Bool) -> Signal, answerCall: @escaping (UUID) -> Void, endCall: @escaping (UUID) -> Signal, setCallMuted: @escaping (UUID, Bool) -> Void, audioSessionActivationChanged: @escaping (Bool) -> Void) { self.audioSessionActivePromise = audioSessionActivePromise self.startCall = startCall self.answerCall = answerCall self.endCall = endCall self.setCallMuted = setCallMuted self.audioSessionActivationChanged = audioSessionActivationChanged } private static func providerConfiguration() -> CXProviderConfiguration { let providerConfiguration = CXProviderConfiguration(localizedName: "Telegram") providerConfiguration.supportsVideo = true providerConfiguration.maximumCallsPerCallGroup = 1 providerConfiguration.maximumCallGroups = 1 providerConfiguration.supportedHandleTypes = [.phoneNumber, .generic] if let image = UIImage(named: "Call/CallKitLogo", in: getAppBundle(), compatibleWith: nil) { providerConfiguration.iconTemplateImageData = image.pngData() } return providerConfiguration } private func requestTransaction(_ transaction: CXTransaction, completion: ((Bool) -> Void)? = nil) { Logger.shared.log("CallKitIntegration", "requestTransaction \(transaction)") self.callController.request(transaction) { error in if let error = error { Logger.shared.log("CallKitIntegration", "error in requestTransaction \(transaction): \(error)") } completion?(error == nil) } } func endCall(uuid: UUID) { Logger.shared.log("CallKitIntegration", "endCall \(uuid)") let endCallAction = CXEndCallAction(call: uuid) let transaction = CXTransaction(action: endCallAction) self.requestTransaction(transaction) } func dropCall(uuid: UUID) { Logger.shared.log("CallKitIntegration", "report call ended \(uuid)") self.provider.reportCall(with: uuid, endedAt: nil, reason: CXCallEndedReason.remoteEnded) } func answerCall(uuid: UUID) { Logger.shared.log("CallKitIntegration", "answer call \(uuid)") let answerCallAction = CXAnswerCallAction(call: uuid) let transaction = CXTransaction(action: answerCallAction) self.requestTransaction(transaction) } func startCall(context: AccountContext, peerId: PeerId, phoneNumber: String?, isVideo: Bool, displayTitle: String) { let uuid = UUID() self.currentStartCallAccount = (uuid, context) let handle: CXHandle if let phoneNumber = phoneNumber { handle = CXHandle(type: .phoneNumber, value: phoneNumber) } else { handle = CXHandle(type: .generic, value: "\(peerId.id._internalGetInt64Value())") } self.uuidToPeerIdMapping[uuid] = peerId let startCallAction = CXStartCallAction(call: uuid, handle: handle) startCallAction.contactIdentifier = displayTitle startCallAction.isVideo = isVideo let transaction = CXTransaction(action: startCallAction) Logger.shared.log("CallKitIntegration", "initiate call \(uuid)") self.requestTransaction(transaction, completion: { _ in let update = CXCallUpdate() update.remoteHandle = handle update.localizedCallerName = displayTitle update.supportsHolding = false update.supportsGrouping = false update.supportsUngrouping = false update.supportsDTMF = false self.provider.reportCall(with: uuid, updated: update) }) } func reportIncomingCall(uuid: UUID, stableId: Int64, handle: String, phoneNumber: String?, isVideo: Bool, displayTitle: String, completion: ((NSError?) -> Void)?) { if self.alreadyReportedIncomingCalls.contains(uuid) { completion?(nil) return } self.alreadyReportedIncomingCalls.insert(uuid) let update = CXCallUpdate() let nativeHandle: CXHandle if let phoneNumber = phoneNumber { nativeHandle = CXHandle(type: .phoneNumber, value: phoneNumber) } else { nativeHandle = CXHandle(type: .generic, value: handle) } update.remoteHandle = nativeHandle update.localizedCallerName = displayTitle update.supportsHolding = false update.supportsGrouping = false update.supportsUngrouping = false update.supportsDTMF = false update.hasVideo = isVideo Logger.shared.log("CallKitIntegration", "report incoming call \(uuid)") OngoingCallContext.setupAudioSession() /*do { try AVAudioSession.sharedInstance().setMode(.voiceChat) } catch let e { print("AVAudioSession.sharedInstance().setMode(.voiceChat) error \(e)") }*/ self.provider.reportNewIncomingCall(with: uuid, update: update, completion: { error in completion?(error as NSError?) }) } func reportOutgoingCallConnecting(uuid: UUID, at date: Date) { Logger.shared.log("CallKitIntegration", "report outgoing call connecting \(uuid)") self.provider.reportOutgoingCall(with: uuid, startedConnectingAt: date) } func reportOutgoingCallConnected(uuid: UUID, at date: Date) { Logger.shared.log("CallKitIntegration", "report call connected \(uuid)") self.provider.reportOutgoingCall(with: uuid, connectedAt: date) } func providerDidReset(_ provider: CXProvider) { Logger.shared.log("CallKitIntegration", "providerDidReset") } func provider(_ provider: CXProvider, perform action: CXStartCallAction) { Logger.shared.log("CallKitIntegration", "provider perform start call action \(action)") guard let startCall = self.startCall, let (uuid, context) = self.currentStartCallAccount, uuid == action.callUUID else { action.fail() return } self.currentStartCallAccount = nil let disposable = MetaDisposable() self.disposableSet.add(disposable) let peerId = self.uuidToPeerIdMapping[action.callUUID] disposable.set((startCall(context, action.callUUID, peerId, action.handle.value, action.isVideo) |> deliverOnMainQueue |> afterDisposed { [weak self, weak disposable] in if let strongSelf = self, let disposable = disposable { strongSelf.disposableSet.remove(disposable) } }).start(next: { result in if result { action.fulfill() } else { action.fail() } })) } func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { Logger.shared.log("CallKitIntegration", "provider perform answer call action \(action)") guard let answerCall = self.answerCall else { action.fail() return } answerCall(action.callUUID) action.fulfill() } func provider(_ provider: CXProvider, perform action: CXEndCallAction) { Logger.shared.log("CallKitIntegration", "provider perform end call action \(action)") guard let endCall = self.endCall else { action.fail() return } let disposable = MetaDisposable() self.disposableSet.add(disposable) disposable.set((endCall(action.callUUID) |> deliverOnMainQueue |> afterDisposed { [weak self, weak disposable] in if let strongSelf = self, let disposable = disposable { strongSelf.disposableSet.remove(disposable) } }).start(next: { result in if result { action.fulfill(withDateEnded: Date()) } else { action.fail() } })) } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { Logger.shared.log("CallKitIntegration", "provider perform mute call action \(action)") guard let setCallMuted = self.setCallMuted else { action.fail() return } setCallMuted(action.uuid, action.isMuted) action.fulfill() } func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { Logger.shared.log("CallKitIntegration", "provider didActivate audio session") self.isAudioSessionActive = true self.audioSessionActivationChanged?(true) self.audioSessionActivePromise?.set(true) if let outputMode = self.pendingVoiceChatOutputMode { self.pendingVoiceChatOutputMode = nil ManagedAudioSession.shared?.applyVoiceChatOutputModeInCurrentAudioSession(outputMode: outputMode) } } func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { Logger.shared.log("CallKitIntegration", "provider didDeactivate audio session") self.isAudioSessionActive = false self.audioSessionActivationChanged?(false) self.audioSessionActivePromise?.set(false) } func applyVoiceChatOutputMode(outputMode: AudioSessionOutputMode) { if self.isAudioSessionActive { ManagedAudioSession.shared?.applyVoiceChatOutputModeInCurrentAudioSession(outputMode: outputMode) } else { self.pendingVoiceChatOutputMode = outputMode } } }