session-ios/Signal/src/call/Speakerbox/CallKitProviderDelegate.swift

260 lines
9.3 KiB
Swift

// Created by Michael Kirk on 12/23/16.
// Copyright © 2016 Open Whisper Systems. All rights reserved.
import Foundation
import UIKit
import CallKit
import AVFoundation
@available(iOS 10.0, *)
final class CallKitProviderDelegate: NSObject, CXProviderDelegate {
let TAG = "[CallKitProviderDelegate]"
let callManager: CallKitCallManager
let callService: CallService
private let provider: CXProvider
// FIXME - I might be thinking about this the wrong way.
// It seems like the provider delegate wants to stop/start the audio recording
// process, but the ProviderDelegate is an app singleton
// and the audio recording process is currently controlled (I think) by
// the PeerConnectionClient instance, which is one per call (NOT a singleton).
// It seems like a mess to reconcile this difference in cardinality. But... here we are.
var audioManager: SignalCallAudioManager?
init(callManager: CallKitCallManager, callService: CallService) {
self.callService = callService
self.callManager = callManager
provider = CXProvider(configuration: type(of: self).providerConfiguration)
super.init()
provider.setDelegate(self, queue: nil)
}
/// The app's provider configuration, representing its CallKit capabilities
static var providerConfiguration: CXProviderConfiguration {
let localizedName = NSLocalizedString("APPLICATION_NAME", comment: "Name of application")
let providerConfiguration = CXProviderConfiguration(localizedName: localizedName)
providerConfiguration.supportsVideo = true
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.supportedHandleTypes = [.phoneNumber]
if let iconMaskImage = UIImage(named: "IconMask") {
providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation(iconMaskImage)
}
providerConfiguration.ringtoneSound = "r.caf"
return providerConfiguration
}
/// Use CXProvider to report the incoming call to the system
func reportIncomingCall(_ call: SignalCall, completion: ((NSError?) -> Void)? = nil) {
// Construct a CXCallUpdate describing the incoming call, including the caller.
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .phoneNumber, value: call.remotePhoneNumber)
update.hasVideo = call.hasVideo
// Report the incoming call to the system
provider.reportNewIncomingCall(with: call.localId, update: update) { error in
/*
Only add incoming call to the app's list of calls if the call was allowed (i.e. there was no error)
since calls may be "denied" for various legitimate reasons. See CXErrorCodeIncomingCallError.
*/
if error == nil {
self.callManager.addCall(call)
}
completion?(error as? NSError)
}
}
// MARK: CXProviderDelegate
func providerDidReset(_ provider: CXProvider) {
Logger.debug("\(TAG) in \(#function)")
stopAudio()
/*
End any ongoing calls if the provider resets, and remove them from the app's list of calls,
since they are no longer valid.
*/
// This is a little goofy because CallKit assumes multiple calls (maybe some are held, or group calls?)
// but CallService currently just has one call at a time.
for call in callManager.calls {
callService.handleFailedCall(error: .providerReset)
}
// Remove all calls from the app's list of calls.
callManager.removeAllCalls()
}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
Logger.debug("\(TAG) in \(#function) CXStartCallAction")
/*
Configure the audio session, but do not start call audio here, since it must be done once
the audio session has been activated by the system after having its priority elevated.
*/
configureAudioSession()
// TODO does this work when `action.handle.value` is not in e164 format, e.g. if called via intent?
guard let call = callManager.callWithLocalId(action.callUUID) else {
Logger.error("\(TAG) unable to find call in \(#function)")
return
}
CallService.signalingQueue.async {
self.callService.handleOutgoingCall(call).then {
action.fulfill()
}.catch { error in
self.callManager.removeCall(call)
action.fail()
}
}
// TODO FIXME
// /*
// Set callback blocks for significant events in the call's lifecycle, so that the CXProvider may be updated
// to reflect the updated state.
// */
// call.hasStartedConnectingDidChange = { [weak self] in
// self?.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: call.connectingDate)
// }
// call.hasConnectedDidChange = { [weak self] in
// self?.provider.reportOutgoingCall(with: call.uuid, connectedAt: call.connectDate)
// }
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
Logger.debug("\(TAG) Received \(#function) CXAnswerCallAction")
// Retrieve the SpeakerboxCall instance corresponding to the action's call UUID
guard let call = callManager.callWithLocalId(action.callUUID) else {
action.fail()
return
}
// Original Speakerbox implementation
// /*
// Configure the audio session, but do not start call audio here, since it must be done once
// the audio session has been activated by the system after having its priority elevated.
// */
// configureAudioSession()
//
// // Trigger the call to be answered via the underlying network service.
// call.answerSpeakerboxCall()
// Synchronous to ensure work is done before call is displayed as "answered"
CallService.signalingQueue.sync {
self.callService.handleAnswerCall(call)
}
// Signal to the system that the action has been successfully performed.
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
Logger.debug("\(TAG) Received \(#function) CXEndCallAction")
guard let call = callManager.callWithLocalId(action.callUUID) else {
action.fail()
return
}
// Original Speakerbox implementation
// // Stop call audio whenever ending the call.
// stopAudio()
// // Trigger the call to be ended via the underlying network service.
// call.endSpeakerboxCall()
// Synchronous to ensure call is terminated before call is displayed as "ended"
CallService.signalingQueue.sync {
self.callService.handleLocalHungupCall(call)
}
// Signal to the system that the action has been successfully performed.
action.fulfill()
// Remove the ended call from the app's list of calls.
callManager.removeCall(call)
}
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
Logger.debug("\(TAG) Received \(#function) CXSetHeldCallAction")
guard let call = callManager.callWithLocalId(action.callUUID) else {
action.fail()
return
}
Logger.warn("TODO, set held call: \(call)")
// TODO FIXME
// // Update the SpeakerboxCall's underlying hold state.
// call.isOnHold = action.isOnHold
//
// // Stop or start audio in response to holding or unholding the call.
// if call.isOnHold {
// stopAudio()
// } else {
// startAudio()
// }
// Signal to the system that the action has been successfully performed.
action.fulfill()
}
func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
Logger.debug("\(TAG) Timed out \(#function)")
// React to the action timeout if necessary, such as showing an error UI.
}
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
Logger.debug("\(TAG) Received \(#function)")
startAudio()
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
Logger.debug("\(TAG) Received \(#function)")
/*
Restart any non-call related audio now that the app's audio session has been
de-activated after having its priority restored to normal.
*/
}
// MARK: - Audio
func startAudio() {
guard let audioManager = self.audioManager else {
Logger.error("\(TAG) audioManager was unexpectedly nil while tryign to start audio")
return
}
audioManager.startAudio()
}
func stopAudio() {
guard let audioManager = self.audioManager else {
Logger.error("\(TAG) audioManager was unexpectedly nil while tryign to stop audio")
return
}
audioManager.stopAudio()
}
func configureAudioSession() {
guard let audioManager = self.audioManager else {
Logger.error("\(TAG) audioManager was unexpectedly nil while trying to: \(#function)")
return
}
audioManager.configureAudioSession()
}
}