import Foundation import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit public final class CallController: ViewController { private var controllerNode: CallControllerNode { return self.displayNode as! CallControllerNode } private let _ready = Promise(false) override public var ready: Promise { return self._ready } private let account: Account public let call: PresentationCall private var presentationData: PresentationData private var animatedAppearance = false private var peer: Peer? private var peerDisposable: Disposable? private var disposable: Disposable? private var callMutedDisposable: Disposable? private var isMuted = false private var audioOutputStateDisposable: Disposable? private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)? public init(account: Account, call: PresentationCall) { self.account = account self.call = call self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .White self.statusBar.ignoreInCall = true self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) self.disposable = (call.state |> deliverOnMainQueue).start(next: { [weak self] callState in self?.callStateUpdated(callState) }) self.callMutedDisposable = (call.isMuted |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { strongSelf.isMuted = value if strongSelf.isNodeLoaded { strongSelf.controllerNode.isMuted = value } } }) self.audioOutputStateDisposable = (call.audioOutputState |> deliverOnMainQueue).start(next: { [weak self] state in if let strongSelf = self { strongSelf.audioOutputState = state if strongSelf.isNodeLoaded { strongSelf.controllerNode.updateAudioOutputs(availableOutputs: state.0, currentOutput: state.1) } } }) } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.peerDisposable?.dispose() self.disposable?.dispose() self.callMutedDisposable?.dispose() self.audioOutputStateDisposable?.dispose() } private func callStateUpdated(_ callState: PresentationCallState) { if self.isNodeLoaded { self.controllerNode.updateCallState(callState) } } override public func loadDisplayNode() { self.displayNode = CallControllerNode(account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit) self.displayNodeDidLoad() self.controllerNode.toggleMute = { [weak self] in self?.call.toggleIsMuted() } self.controllerNode.setCurrentAudioOutput = { [weak self] output in self?.call.setCurrentAudioOutput(output) } self.controllerNode.beginAudioOuputSelection = { [weak self] in guard let strongSelf = self, let (availableOutputs, currentOutput) = strongSelf.audioOutputState else { return } guard availableOutputs.count >= 2 else { return } if availableOutputs.count == 2 { for output in availableOutputs { if output != currentOutput { strongSelf.call.setCurrentAudioOutput(output) break } } } else { let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) var items: [ActionSheetItem] = [] for output in availableOutputs { let title: String var icon: UIImage? switch output { case .builtin: title = UIDevice.current.model case .speaker: title = strongSelf.presentationData.strings.Call_AudioRouteSpeaker icon = UIImage(bundleImageName: "Call/CallRouteSpeaker") case .headphones: title = strongSelf.presentationData.strings.Call_AudioRouteHeadphones case let .port(port): title = port.name if port.type == .bluetooth { icon = UIImage(bundleImageName: "Call/CallRouteBluetooth") } } items.append(CallRouteActionSheetItem(title: title, icon: icon, selected: output == currentOutput, action: { [weak actionSheet] in actionSheet?.dismissAnimated() self?.call.setCurrentAudioOutput(output) })) } actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ]) ]) strongSelf.present(actionSheet, in: .window(.root)) } } self.controllerNode.acceptCall = { [weak self] in let _ = self?.call.answer() } self.controllerNode.endCall = { [weak self] in let _ = self?.call.hangUp() } self.controllerNode.back = { [weak self] in let _ = self?.dismiss() } self.controllerNode.dismissedInteractively = { [weak self] in self?.animatedAppearance = false self?.presentingViewController?.dismiss(animated: false, completion: nil) } self.peerDisposable = (account.postbox.peerView(id: self.call.peerId) |> deliverOnMainQueue).start(next: { [weak self] view in if let strongSelf = self { if let peer = view.peers[view.peerId] { strongSelf.peer = peer strongSelf.controllerNode.updatePeer(peer: peer) strongSelf._ready.set(.single(true)) } } }) self.controllerNode.isMuted = self.isMuted if let audioOutputState = self.audioOutputState { self.controllerNode.updateAudioOutputs(availableOutputs: audioOutputState.0, currentOutput: audioOutputState.1) } } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !self.animatedAppearance { self.animatedAppearance = true self.controllerNode.animateIn() } } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } override public func dismiss(completion: (() -> Void)? = nil) { self.controllerNode.animateOut(completion: { [weak self] in self?.animatedAppearance = false self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() }) } @objc func backPressed() { self.dismiss() } }