import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SyncCore import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences import TelegramVoip import TelegramAudio import AccountContext import TelegramNotices import AppBundle 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?)? private let idleTimerExtensionDisposable = MetaDisposable() 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() self.idleTimerExtensionDisposable.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, call: self.call) 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(presentationData: strongSelf.presentationData) 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, font: .bold, 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: {}) } }, push: { [weak self] c in if let strongSelf = self { strongSelf.push(c) } }) 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 { Queue.mainQueue().after(1.0) { let controller = callSuggestTabController(sharedContext: strongSelf.sharedContext) strongSelf.present(controller, in: .window(.root)) } } if callsTabTip < 3 { let _ = ApplicationSpecificNotice.incrementCallsTabTips(accountManager: strongSelf.sharedContext.accountManager).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() } self.idleTimerExtensionDisposable.set(self.sharedContext.applicationBindings.pushIdleTimerExtension()) } override public func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.idleTimerExtensionDisposable.set(nil) } 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() } }