import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences import TelegramVoip import TelegramAudio import AccountContext import TelegramNotices 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 sharedContext: SharedAccountContext private let account: Account public let call: PresentationCall private let easyDebugAccess: Bool private var presentationData: PresentationData private var didPlayPresentationAnimation = false private var peer: Peer? private var peerDisposable: Disposable? private var disposable: Disposable? private var callMutedDisposable: Disposable? private var isMuted = false private var presentedCallRating = false private var audioOutputStateDisposable: Disposable? private var audioOutputState: ([AudioSessionOutput], AudioSessionOutput?)? public init(sharedContext: SharedAccountContext, account: Account, call: PresentationCall, easyDebugAccess: Bool) { self.sharedContext = sharedContext self.account = account self.call = call self.easyDebugAccess = easyDebugAccess self.presentationData = sharedContext.currentPresentationData.with { $0 } super.init(navigationBarPresentationData: nil) self.isOpaqueWhenInOverlay = true 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(sharedContext: self.sharedContext, account: self.account, presentationData: self.presentationData, statusBar: self.statusBar, debugInfo: self.call.debugInfo(), shouldStayHiddenUntilConnection: !self.call.isOutgoing && self.call.isIntegratedWithCallKit, easyDebugAccess: self.easyDebugAccess) 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(.calls)) } } 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.presentCallRating = { [weak self] callId in if let strongSelf = self, !strongSelf.presentedCallRating { strongSelf.presentedCallRating = true Queue.mainQueue().after(0.5, { let window = strongSelf.window let controller = callRatingController(sharedContext: strongSelf.sharedContext, account: strongSelf.account, callId: callId, userInitiated: false, present: { c, a in if let window = window { c.presentationArguments = a window.present(c, on: .root, blockInteraction: false, completion: {}) } }) strongSelf.present(controller, in: .window(.root)) }) } } self.controllerNode.callEnded = { [weak self] didPresentRating in if let strongSelf = self, !didPresentRating { let _ = (combineLatest(strongSelf.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.callListSettings]), ApplicationSpecificNotice.getCallsTabTip(accountManager: strongSelf.sharedContext.accountManager)) |> map { sharedData, callsTabTip -> Int32 in var value = false if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.callListSettings] as? CallListSettings { value = settings.showTab } if value { return 3 } else { return callsTabTip } } |> deliverOnMainQueue).start(next: { [weak self] callsTabTip in if let strongSelf = self { if callsTabTip == 2 { let controller = callSuggestTabController(sharedContext: strongSelf.sharedContext) strongSelf.present(controller, in: .window(.root)) } if callsTabTip < 3 { let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.sharedContext.accountManager, count: 4).start() } } }) } } self.controllerNode.dismissedInteractively = { [weak self] in self?.didPlayPresentationAnimation = false self?.presentingViewController?.dismiss(animated: false, completion: nil) } self.peerDisposable = (combineLatest(self.account.postbox.peerView(id: self.account.peerId) |> take(1), self.account.postbox.peerView(id: self.call.peerId), self.sharedContext.activeAccountsWithInfo |> take(1)) |> deliverOnMainQueue).start(next: { [weak self] accountView, view, activeAccountsWithInfo in if let strongSelf = self { if let accountPeer = accountView.peers[accountView.peerId], let peer = view.peers[view.peerId] { strongSelf.peer = peer strongSelf.controllerNode.updatePeer(accountPeer: accountPeer, peer: peer, hasOther: activeAccountsWithInfo.accounts.count > 1) 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.didPlayPresentationAnimation { self.didPlayPresentationAnimation = 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?.didPlayPresentationAnimation = false self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() }) } @objc func backPressed() { self.dismiss() } }