import Foundation import SwiftSignalKit import TelegramCore import Postbox import TelegramUIPreferences import libtgvoip private func callConnectionDescription(_ connection: CallSessionConnection) -> OngoingCallConnectionDescription { return OngoingCallConnectionDescription(connectionId: connection.id, ip: connection.ip, ipv6: connection.ipv6, port: connection.port, peerTag: connection.peerTag) } private let callLogsLimit = 20 public func callLogNameForId(id: Int64, account: Account) -> String? { let path = callLogsPath(account: account) let namePrefix = "\(id)_" if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { for url in enumerator { if let url = url as? URL { if url.lastPathComponent.hasPrefix(namePrefix) { return url.lastPathComponent } } } } return nil } public func callLogsPath(account: Account) -> String { return account.basePath + "/calls" } private func cleanupCallLogs(account: Account) { let path = callLogsPath(account: account) let fileManager = FileManager.default if !fileManager.fileExists(atPath: path, isDirectory: nil) { try? fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil) } var oldest: (URL, Date)? = nil var count = 0 if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles, .skipsSubdirectoryDescendants], errorHandler: nil) { for url in enumerator { if let url = url as? URL { if let date = (try? url.resourceValues(forKeys: Set([.contentModificationDateKey])))?.contentModificationDate { if let currentOldest = oldest { if date < currentOldest.1 { oldest = (url, date) } } else { oldest = (url, date) } count += 1 } } } } if count > callLogsLimit, let oldest = oldest { try? fileManager.removeItem(atPath: oldest.0.path) } } private let setupLogs: Bool = { OngoingCallThreadLocalContext.setupLoggingFunction({ value in if let value = value { Logger.shared.log("TGVOIP", value) } }) return true }() public enum OngoingCallContextState { case initializing case connected case failed } private final class OngoingCallThreadLocalContextQueueImpl: NSObject, OngoingCallThreadLocalContextQueue { private let queue: Queue init(queue: Queue) { self.queue = queue super.init() } func dispatch(_ f: @escaping () -> Void) { self.queue.async { f() } } func isCurrent() -> Bool { return self.queue.isCurrent() } } private func ongoingNetworkTypeForType(_ type: NetworkType) -> OngoingCallNetworkType { switch type { case .none: return .wifi case .wifi: return .wifi case let .cellular(cellular): switch cellular { case .edge: return .cellularEdge case .gprs: return .cellularGprs case .thirdG, .unknown: return .cellular3g case .lte: return .cellularLte } } } private func ongoingDataSavingForType(_ type: VoiceCallDataSaving) -> OngoingCallDataSaving { switch type { case .never: return .never case .cellular: return .cellular case .always: return .always default: return .never } } public final class OngoingCallContext { public let internalId: CallSessionInternalId private let queue = Queue() private let account: Account private let callSessionManager: CallSessionManager private var contextRef: Unmanaged? private let contextState = Promise(nil) public var state: Signal { return self.contextState.get() |> map { $0.flatMap { switch $0 { case .initializing: return .initializing case .connected: return .connected case .failed: return .failed default: return .failed } } } } private let receptionPromise = Promise(nil) public var reception: Signal { return self.receptionPromise.get() } private let audioSessionDisposable = MetaDisposable() private var networkTypeDisposable: Disposable? public static var maxLayer: Int32 { return OngoingCallThreadLocalContext.maxLayer() } public init(account: Account, callSessionManager: CallSessionManager, internalId: CallSessionInternalId, proxyServer: ProxyServerSettings?, initialNetworkType: NetworkType, updatedNetworkType: Signal, serializedData: String?, dataSaving: VoiceCallDataSaving, derivedState: VoipDerivedState) { let _ = setupLogs OngoingCallThreadLocalContext.applyServerConfig(serializedData) self.internalId = internalId self.account = account self.callSessionManager = callSessionManager let queue = self.queue self.queue.async { var voipProxyServer: VoipProxyServer? if let proxyServer = proxyServer { switch proxyServer.connection { case let .socks5(username, password): voipProxyServer = VoipProxyServer(host: proxyServer.host, port: proxyServer.port, username: username, password: password) case .mtp: break } } let context = OngoingCallThreadLocalContext(queue: OngoingCallThreadLocalContextQueueImpl(queue: queue), proxy: voipProxyServer, networkType: ongoingNetworkTypeForType(initialNetworkType), dataSaving: ongoingDataSavingForType(dataSaving), derivedState: derivedState.data) self.contextRef = Unmanaged.passRetained(context) context.stateChanged = { [weak self] state in self?.contextState.set(.single(state)) } context.signalBarsChanged = { [weak self] signalBars in self?.receptionPromise.set(.single(signalBars)) } } self.networkTypeDisposable = (updatedNetworkType |> deliverOn(self.queue)).start(next: { [weak self] networkType in self?.withContext { context in context.setNetworkType(ongoingNetworkTypeForType(networkType)) } }) cleanupCallLogs(account: account) } deinit { let contextRef = self.contextRef self.queue.async { contextRef?.release() } self.audioSessionDisposable.dispose() self.networkTypeDisposable?.dispose() } private func withContext(_ f: @escaping (OngoingCallThreadLocalContext) -> Void) { self.queue.async { if let contextRef = self.contextRef { let context = contextRef.takeUnretainedValue() f(context) } } } public func start(key: Data, isOutgoing: Bool, connections: CallSessionConnectionSet, maxLayer: Int32, allowP2P: Bool, audioSessionActive: Signal, logName: String) { let logPath = logName.isEmpty ? "" : callLogsPath(account: self.account) + "/" + logName + ".log" self.audioSessionDisposable.set((audioSessionActive |> filter { $0 } |> take(1)).start(next: { [weak self] _ in if let strongSelf = self { strongSelf.withContext { context in context.start(withKey: key, isOutgoing: isOutgoing, primaryConnection: callConnectionDescription(connections.primary), alternativeConnections: connections.alternatives.map(callConnectionDescription), maxLayer: maxLayer, allowP2P: allowP2P, logPath: logPath) } } })) } public func stop(callId: CallId? = nil, sendDebugLogs: Bool = false) { self.withContext { context in context.stop { debugLog, bytesSentWifi, bytesReceivedWifi, bytesSentMobile, bytesReceivedMobile in let delta = NetworkUsageStatsConnectionsEntry( cellular: NetworkUsageStatsDirectionsEntry( incoming: bytesReceivedMobile, outgoing: bytesSentMobile), wifi: NetworkUsageStatsDirectionsEntry( incoming: bytesReceivedWifi, outgoing: bytesSentWifi)) updateAccountNetworkUsageStats(account: self.account, category: .call, delta: delta) if let callId = callId, let debugLog = debugLog, sendDebugLogs { let _ = saveCallDebugLog(account: self.account, callId: callId, log: debugLog).start() } } let derivedState = context.getDerivedState() let _ = updateVoipDerivedStateInteractively(postbox: self.account.postbox, { _ in return VoipDerivedState(data: derivedState) }).start() } } public func setIsMuted(_ value: Bool) { self.withContext { context in context.setIsMuted(value) } } public func debugInfo() -> Signal<(String, String), NoError> { let poll = Signal<(String, String), NoError> { subscriber in self.withContext { context in let version = context.version() let debugInfo = context.debugInfo() if let version = version, let debugInfo = debugInfo { subscriber.putNext((version, debugInfo)) } subscriber.putCompletion() } return EmptyDisposable } return (poll |> then(.complete() |> delay(0.5, queue: Queue.concurrentDefaultQueue()))) |> restart } public func needsRating(_ completion: @escaping (Bool) -> Void) { self.withContext { context in let needsRating = context.needRate() Queue.mainQueue().async { completion(needsRating) } } } }