Swiftgram/submodules/TelegramVoip/Sources/OngoingCallContext.swift
2019-08-27 00:44:36 +04:00

298 lines
11 KiB
Swift

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<OngoingCallThreadLocalContext>?
private let contextState = Promise<OngoingCallState?>(nil)
public var state: Signal<OngoingCallContextState?, NoError> {
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<Int32?>(nil)
public var reception: Signal<Int32?, NoError> {
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<NetworkType, NoError>, 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<Bool, NoError>, 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)
}
}
}
}