element-ios/Riot/Managers/Call/CallService.swift

413 lines
14 KiB
Swift
Raw Normal View History

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? {
didSet {
updateOnHoldCall()
}
}
2020-12-01 10:41:31 +00:00
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
}
private var onHoldCallVCs: [CallViewController] {
return callVCs.values.filter { (callVC) -> Bool in
guard let call = callVC.mxCall else {
return false
}
return call.isOnHold
}
}
2020-12-03 13:05:08 +00:00
private var numberOfPausedCalls: UInt {
return UInt(callVCs.values.filter { (callVC) -> Bool in
guard let call = callVC.mxCall else {
return false
}
return call.isOnHold
}.count)
}
2020-12-02 20:17:06 +00:00
// 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 updateOnHoldCall() {
guard let presentedCallVC = presentedCallVC else {
return
}
if onHoldCallVCs.isEmpty {
// no on hold calls, clear the call
presentedCallVC.mxCallOnHold = nil
} else {
for callVC in onHoldCallVCs where callVC != presentedCallVC {
// do not set the same call (can happen in case of two on hold calls)
presentedCallVC.mxCallOnHold = callVC.mxCall
break
}
}
}
2020-12-02 20:17:06 +00:00
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 callHolded(withCallId callId: String) {
updateOnHoldCall()
}
2020-12-02 20:17:06 +00:00
private func endCall(withCallId callId: String) {
guard let callVC = callVCs[callId] else {
return
}
let completion = { [weak self] in
2020-12-14 11:34:48 +00:00
guard let self = self else {
return
}
self.updateOnHoldCall()
2020-12-14 11:34:48 +00:00
self.callVCs.removeValue(forKey: callId)
2020-12-02 20:17:06 +00:00
callVC.destroy()
2020-12-14 11:34:48 +00:00
self.callBackgroundTasks[callId]?.stop()
self.callBackgroundTasks.removeValue(forKey: callId)
// if still have some calls and there is no present operation in the queue
if let oldCallVC = self.callVCs.values.first,
self.presentedCallVC == nil,
2020-12-14 11:34:48 +00:00
!self.uiOperationQueue.containsPresentCallVCOperation,
!self.uiOperationQueue.containsPresentCallBarOperation {
// present the call bar after dismissing this one
self.presentCallBar(for: oldCallVC)
}
2020-12-02 20:17:06 +00:00
}
if inBarCallVC == callVC {
// this call currently in the status bar,
// first present it and then dismiss it
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
}
2020-12-03 13:05:08 +00:00
guard let call = inBarCallVC.mxCall else {
return
}
guard call.state != .ended else {
return
}
2020-12-02 20:17:06 +00:00
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:
2020-12-03 13:05:08 +00:00
NSLog("[CallService] callStateChanged: call created answer: \(call.callId)")
2020-12-01 10:41:31 +00:00
if call.isIncoming, isCallKitEnabled, let callVC = callVCs[call.callId] {
presentCallVC(callVC)
}
2020-12-02 20:17:06 +00:00
case .connected:
2020-12-03 13:05:08 +00:00
NSLog("[CallService] callStateChanged: call connected: \(call.callId)")
2020-12-02 20:17:06 +00:00
callTimer.fire()
case .onHold:
NSLog("[CallService] callStateChanged: call holded: \(call.callId)")
callTimer.fire()
callHolded(withCallId: call.callId)
2020-12-03 13:05:08 +00:00
case .remotelyOnHold:
2020-12-02 20:17:06 +00:00
NSLog("[CallService] callStateChanged: call remotely holded: \(call.callId)")
2020-12-03 13:05:08 +00:00
callTimer.fire()
callHolded(withCallId: call.callId)
2020-12-01 10:41:31 +00:00
case .ended:
NSLog("[CallService] callStateChanged: call ended: \(call.callId)")
endCall(withCallId: call.callId)
default:
break
}
}
// MARK: - Call Screens
private func presentCallVC(_ callVC: CallViewController, completion: (() -> Void)? = nil) {
NSLog("[CallService] presentCallVC: call: \(String(describing: callVC.mxCall?.callId))")
2020-12-03 13:05:08 +00:00
if let inBarCallVC = inBarCallVC {
dismissCallBar(for: inBarCallVC)
}
2020-12-01 10:41:31 +00:00
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 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)
}
}
2020-12-21 14:18:38 +00:00
func callViewControllerDidTap(onHoldCall callViewController: MXKCallViewController!) {
guard let callOnHold = callViewController.mxCallOnHold else {
return
}
guard let onHoldCallVC = callVCs[callOnHold.callId] else {
return
}
if callOnHold.state == .onHold {
// call is on hold locally, switch calls
callViewController.mxCall.hold(true)
callOnHold.hold(false)
}
// switch screens
presentCallVC(onHoldCallVC)
}
2020-12-01 10:41:31 +00:00
}
2020-12-14 11:34:48 +00:00
// MARK: - OperationQueue Extension
extension OperationQueue {
var containsPresentCallVCOperation: Bool {
return operations.contains { (operation) -> Bool in
return operation is CallVCPresentOperation
}
}
var containsPresentCallBarOperation: Bool {
return operations.contains { (operation) -> Bool in
return operation is CallBarPresentOperation
}
}
}