2020-12-01 10:41:31 +00:00
|
|
|
//
|
|
|
|
// Copyright 2020 New Vector Ltd
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import MatrixKit
|
|
|
|
|
|
|
|
@objcMembers
|
2020-12-02 20:17:06 +00:00
|
|
|
/// Service to manage call screens and call bar UI management.
|
2020-12-01 10:41:31 +00:00
|
|
|
class CallService: NSObject {
|
|
|
|
|
|
|
|
private var callVCs: [String: CallViewController] = [:]
|
|
|
|
private var callBackgroundTasks: [String: MXBackgroundTask] = [:]
|
|
|
|
private weak var presentedCallVC: CallViewController?
|
|
|
|
private weak var inBarCallVC: CallViewController?
|
|
|
|
private var uiOperationQueue: OperationQueue = .main
|
|
|
|
private var isStarted: Bool = false
|
2020-12-02 20:17:06 +00:00
|
|
|
private var callTimer: Timer!
|
2020-12-01 10:41:31 +00:00
|
|
|
|
|
|
|
private var isCallKitEnabled: Bool {
|
|
|
|
MXCallKitAdapter.callKitAvailable() && MXKAppSettings.standard()?.isCallKitEnabled == true
|
|
|
|
}
|
|
|
|
|
2020-12-02 20:17:06 +00:00
|
|
|
private var activeCallVC: CallViewController? {
|
|
|
|
return callVCs.values.filter { (callVC) -> Bool in
|
|
|
|
guard let call = callVC.mxCall else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return !call.isOnHold
|
|
|
|
}.first
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Public
|
|
|
|
|
2020-12-01 10:41:31 +00:00
|
|
|
/// Maximum number of concurrent calls allowed.
|
|
|
|
let maximumNumberOfConcurrentCalls: UInt = 2
|
|
|
|
|
|
|
|
/// Delegate object
|
|
|
|
weak var delegate: CallServiceDelegate?
|
|
|
|
|
|
|
|
/// Start the service
|
|
|
|
func start() {
|
|
|
|
addCallObservers()
|
2020-12-02 20:17:06 +00:00
|
|
|
startCallTimer()
|
2020-12-01 10:41:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Stop the service
|
|
|
|
func stop() {
|
|
|
|
removeCallObservers()
|
2020-12-02 20:17:06 +00:00
|
|
|
stopCallTimer()
|
2020-12-01 10:41:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Method to be called when the call status bar is tapped.
|
|
|
|
/// - Returns: If the user interaction handled or not
|
|
|
|
func callStatusBarButtonTapped() -> Bool {
|
2020-12-02 20:17:06 +00:00
|
|
|
if let callVC = inBarCallVC ?? activeCallVC {
|
|
|
|
dismissCallBar(for: callVC)
|
|
|
|
presentCallVC(callVC)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
|
|
private func shouldHandleCall(_ call: MXCall) -> Bool {
|
|
|
|
if let delegate = delegate, !delegate.callService(self, shouldHandleNewCall: call) {
|
2020-12-01 10:41:31 +00:00
|
|
|
return false
|
|
|
|
}
|
2020-12-02 20:17:06 +00:00
|
|
|
return callVCs.count < maximumNumberOfConcurrentCalls
|
|
|
|
}
|
|
|
|
|
|
|
|
private func endCall(withCallId callId: String) {
|
|
|
|
guard let callVC = callVCs[callId] else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let completion = { [weak self] in
|
|
|
|
self?.callVCs.removeValue(forKey: callId)
|
|
|
|
callVC.destroy()
|
|
|
|
self?.callBackgroundTasks[callId]?.stop()
|
|
|
|
self?.callBackgroundTasks.removeValue(forKey: callId)
|
|
|
|
}
|
|
|
|
|
|
|
|
if inBarCallVC == callVC {
|
|
|
|
// this call currently in the status bar,
|
|
|
|
// first present it and then dismiss it
|
|
|
|
dismissCallBar(for: callVC)
|
|
|
|
presentCallVC(callVC)
|
|
|
|
}
|
|
|
|
dismissCallVC(callVC, completion: completion)
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Timer
|
|
|
|
|
|
|
|
private func startCallTimer() {
|
|
|
|
callTimer = Timer.scheduledTimer(timeInterval: 1.0,
|
|
|
|
target: self,
|
|
|
|
selector: #selector(callTimerFired(_:)),
|
|
|
|
userInfo: nil,
|
|
|
|
repeats: true)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func stopCallTimer() {
|
|
|
|
callTimer.invalidate()
|
|
|
|
callTimer = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc private func callTimerFired(_ timer: Timer) {
|
|
|
|
guard let inBarCallVC = inBarCallVC else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
presentCallBar(for: inBarCallVC, isUpdateOnly: true)
|
2020-12-01 10:41:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Observers
|
|
|
|
|
|
|
|
private func addCallObservers() {
|
|
|
|
guard !isStarted else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
|
|
selector: #selector(newCall(_:)),
|
|
|
|
name: NSNotification.Name(rawValue: kMXCallManagerNewCall),
|
|
|
|
object: nil)
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
|
|
selector: #selector(callStateChanged(_:)),
|
|
|
|
name: NSNotification.Name(rawValue: kMXCallStateDidChange),
|
|
|
|
object: nil)
|
|
|
|
|
|
|
|
isStarted = true
|
|
|
|
}
|
|
|
|
|
|
|
|
private func removeCallObservers() {
|
|
|
|
guard isStarted else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
NotificationCenter.default.removeObserver(self,
|
|
|
|
name: NSNotification.Name(rawValue: kMXCallManagerNewCall),
|
|
|
|
object: nil)
|
|
|
|
NotificationCenter.default.removeObserver(self,
|
|
|
|
name: NSNotification.Name(rawValue: kMXCallStateDidChange),
|
|
|
|
object: nil)
|
|
|
|
|
|
|
|
isStarted = false
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc
|
|
|
|
private func newCall(_ notification: Notification) {
|
|
|
|
guard let call = notification.object as? MXCall else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !shouldHandleCall(call) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let newCallVC = CallViewController(call) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
newCallVC.playRingtone = !isCallKitEnabled
|
|
|
|
newCallVC.delegate = self
|
|
|
|
callVCs[call.callId] = newCallVC
|
|
|
|
|
|
|
|
if UIApplication.shared.applicationState == .background && call.isIncoming {
|
|
|
|
// Create backgound task.
|
|
|
|
// Without CallKit this will allow us to play vibro until the call was ended
|
|
|
|
// With CallKit we'll inform the system when the call is ended to let the system terminate our app to save resources
|
|
|
|
let handler = MXSDKOptions.sharedInstance().backgroundModeHandler
|
|
|
|
let callBackgroundTask = handler.startBackgroundTask(withName: "[CallService] addMatrixCallObserver", expirationHandler: nil)
|
|
|
|
|
|
|
|
callBackgroundTasks[call.callId] = callBackgroundTask
|
|
|
|
}
|
|
|
|
|
|
|
|
if call.isIncoming && isCallKitEnabled {
|
|
|
|
return
|
|
|
|
} else {
|
|
|
|
presentCallVC(newCallVC)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc
|
|
|
|
private func callStateChanged(_ notification: Notification) {
|
|
|
|
guard let call = notification.object as? MXCall else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
switch call.state {
|
|
|
|
case .createAnswer:
|
|
|
|
if call.isIncoming, isCallKitEnabled, let callVC = callVCs[call.callId] {
|
|
|
|
presentCallVC(callVC)
|
2020-12-02 20:17:06 +00:00
|
|
|
return
|
2020-12-01 10:41:31 +00:00
|
|
|
}
|
|
|
|
NSLog("[CallService] callStateChanged: call created answer: \(call.callId)")
|
2020-12-02 20:17:06 +00:00
|
|
|
case .connected:
|
|
|
|
callTimer.fire()
|
|
|
|
case .onHold:
|
|
|
|
callTimer.fire()
|
|
|
|
NSLog("[CallService] callStateChanged: call holded: \(call.callId)")
|
|
|
|
case .remotelyOnHold:
|
|
|
|
callTimer.fire()
|
|
|
|
NSLog("[CallService] callStateChanged: call remotely holded: \(call.callId)")
|
2020-12-01 10:41:31 +00:00
|
|
|
case .ended:
|
|
|
|
NSLog("[CallService] callStateChanged: call ended: \(call.callId)")
|
|
|
|
endCall(withCallId: call.callId)
|
2020-12-02 20:17:06 +00:00
|
|
|
return
|
2020-12-01 10:41:31 +00:00
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Call Screens
|
|
|
|
|
|
|
|
private func presentCallVC(_ callVC: CallViewController, completion: (() -> Void)? = nil) {
|
|
|
|
NSLog("[CallService] presentCallVC: call: \(String(describing: callVC.mxCall?.callId))")
|
|
|
|
|
|
|
|
if let presentedCallVC = presentedCallVC {
|
|
|
|
dismissCallVC(presentedCallVC)
|
|
|
|
}
|
|
|
|
|
|
|
|
let operation = CallVCPresentOperation(service: self, callVC: callVC) { [weak self] in
|
|
|
|
self?.presentedCallVC = callVC
|
|
|
|
completion?()
|
|
|
|
}
|
|
|
|
uiOperationQueue.addOperation(operation)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func dismissCallVC(_ callVC: CallViewController, completion: (() -> Void)? = nil) {
|
|
|
|
NSLog("[CallService] dismissCallVC: call: \(String(describing: callVC.mxCall?.callId))")
|
|
|
|
|
|
|
|
let operation = CallVCDismissOperation(service: self, callVC: callVC) { [weak self] in
|
|
|
|
if callVC == self?.presentedCallVC {
|
|
|
|
self?.presentedCallVC = nil
|
|
|
|
}
|
|
|
|
completion?()
|
|
|
|
}
|
|
|
|
uiOperationQueue.addOperation(operation)
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Call Bar
|
|
|
|
|
2020-12-02 20:17:06 +00:00
|
|
|
private func presentCallBar(for callVC: CallViewController?, isUpdateOnly: Bool = false, completion: (() -> Void)? = nil) {
|
|
|
|
NSLog("[CallService] presentCallBar: call: \(String(describing: callVC?.mxCall?.callId))")
|
|
|
|
|
|
|
|
let activeCallVC = self.activeCallVC
|
2020-12-01 10:41:31 +00:00
|
|
|
|
2020-12-02 20:17:06 +00:00
|
|
|
let numberOfPausedCalls = UInt(callVCs.values.filter { (callVC) -> Bool in
|
|
|
|
guard let call = callVC.mxCall else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return call.isOnHold
|
|
|
|
}.count)
|
|
|
|
|
|
|
|
let operation = CallBarPresentOperation(service: self, activeCallVC: activeCallVC, numberOfPausedCalls: numberOfPausedCalls) { [weak self] in
|
|
|
|
// active calls are more prior to paused ones.
|
|
|
|
// So, if user taps the bar when we have one active and one paused calls, we navigate to the active one.
|
|
|
|
if !isUpdateOnly {
|
|
|
|
self?.inBarCallVC = activeCallVC ?? callVC
|
|
|
|
}
|
2020-12-01 10:41:31 +00:00
|
|
|
completion?()
|
|
|
|
}
|
|
|
|
uiOperationQueue.addOperation(operation)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func dismissCallBar(for callVC: CallViewController, completion: (() -> Void)? = nil) {
|
|
|
|
NSLog("[CallService] dismissCallBar: call: \(String(describing: callVC.mxCall?.callId))")
|
|
|
|
|
2020-12-02 20:17:06 +00:00
|
|
|
let operation = CallBarDismissOperation(service: self) { [weak self] in
|
2020-12-01 10:41:31 +00:00
|
|
|
if callVC == self?.inBarCallVC {
|
|
|
|
self?.inBarCallVC = nil
|
|
|
|
}
|
|
|
|
completion?()
|
|
|
|
}
|
2020-12-02 20:17:06 +00:00
|
|
|
|
2020-12-01 10:41:31 +00:00
|
|
|
uiOperationQueue.addOperation(operation)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-12-02 20:17:06 +00:00
|
|
|
// MARK: - MXKCallViewControllerDelegate
|
|
|
|
|
2020-12-01 10:41:31 +00:00
|
|
|
extension CallService: MXKCallViewControllerDelegate {
|
|
|
|
|
|
|
|
func dismiss(_ callViewController: MXKCallViewController!, completion: (() -> Void)!) {
|
|
|
|
guard let callVC = callViewController as? CallViewController else {
|
|
|
|
// this call screen is not handled by this service
|
|
|
|
completion?()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if callVC.mxCall == nil || callVC.mxCall.state == .ended {
|
|
|
|
// wait for the call state changes, will be handled there
|
|
|
|
return
|
|
|
|
} else {
|
|
|
|
dismissCallVC(callVC)
|
|
|
|
self.presentCallBar(for: callVC, completion: completion)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|