In order to integrate VoIP push messages into your application, you need to make the following changes.
Go to your project -> your target -> Signing & Capabilities
-> + Capability
.
Add Background Modes
.
And Push Notifications
.
Further it is necessary to mark the following points: Audio
, Voice over IP
, and Remote notifications
.
You need to link CallKit
and PushKit
frameworks.
import AVFoundation
import CallKit
import PushKit
import SmartSpacesKit
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder {
...
let intercomHandler = Intercom.Handler()
}
extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
#if DEBUG
UserDeviceManager.shared().pushEnvironment = .development
#else
UserDeviceManager.shared().pushEnvironment = .production
#endif
...
return intercomHandler.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
intercomHandler.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
}
Create the intercom handler
import AVFoundation
import CallKit
import Foundation
import JitsiMeetSDK
import PushKit
import SmartSpacesKit
extension Intercom {
class Handler: NSObject {
let callProvider: CXProvider = {
let configuration: CXProviderConfiguration
if #available(iOS 14, *) {
configuration = CXProviderConfiguration()
} else {
configuration = CXProviderConfiguration(localizedName: "VoIP Service")
}
configuration.supportsVideo = true
configuration.supportedHandleTypes = [.generic]
configuration.maximumCallsPerCallGroup = 1
configuration.iconTemplateImageData = UIImage(named: "icon name")?.pngData()
return CXProvider(configuration: configuration)
}()
var currentIntercomDevice: Intercom.Device?
let intercomManager = SWSmartLockManager.shared().intercomManager
var isCallStarted = false
let jitsiView = JitsiMeetView(frame: .zero)
let pushRegistry = PKPushRegistry(queue: nil)
let userDeviceManager = UserDeviceManager.shared()
override init() {
super.init()
callProvider.setDelegate(self, queue: nil)
intercomManager.delegate = self
jitsiView.delegate = self
pushRegistry.delegate = self
pushRegistry.desiredPushTypes = [.voIP]
}
}
}
Extension for supporting fast Jitsi initialization
extension Intercom.Handler {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return JitsiMeet.sharedInstance().application(application, didFinishLaunchingWithOptions: launchOptions ?? [:])
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == "INStartVideoCallIntent" else {
return false
}
return JitsiMeet.sharedInstance().application(application, continue: userActivity, restorationHandler: restorationHandler)
}
}
Intercom.Handler
main actions:
extension Intercom.Handler {
func showCallScreen() {
guard currentIntercomDevice != nil else { return }
// Display calling screen
}
func hideCallScreen() {
// Hiding the calling screen
}
func startAudio() {
guard
let audioToken = currentIntercomDevice?.audio?.token,
let audioUrl = currentIntercomDevice?.audio?.url
else {
print("No audio token and/or URL provided")
return
}
let callHandle = currentIntercomDevice?.name
let callUUID = currentIntercomDevice?.id
let options = JitsiMeetConferenceOptions.fromBuilder { builder in
builder.audioMuted = false
builder.audioOnly = true
builder.callHandle = callHandle
builder.callUUID = callUUID
builder.room = audioUrl.lastPathComponent
builder.serverURL = audioUrl
builder.token = audioToken
builder.welcomePageEnabled = false
}
jitsiView.join(options)
}
func stopAudio() {
jitsiView.hangUp()
}
func configureAudioSession(completion: @escaping (Bool) -> Void) {
let audioSession = AVAudioSession.sharedInstance()
audioSession.requestRecordPermission { granted in
if granted {
do {
if audioSession.category != .playAndRecord {
try audioSession.setCategory(.playAndRecord, options: .defaultToSpeaker)
}
if audioSession.mode != .voiceChat {
try audioSession.setMode(.voiceChat)
}
} catch {
print("Error configuring AVAudioSession: \(error.localizedDescription)")
}
} else {
print("Mic permission is not granted")
}
completion(granted)
}
}
// The call ends by user
func endCurrentCall() {
guard let intercomDevice = currentIntercomDevice else {
print("End current call: No current intercom device")
return
}
hideCallScreen()
stopAudio()
callProvider.reportCall(with: intercomDevice.id, endedAt: nil, reason: .remoteEnded)
intercomManager.report(callStatus: .end, for: intercomDevice) { error in
if let error = error {
print("Reporting the call status \(Intercom.CallStatus.end) failed with error: \(error)")
}
}
currentIntercomDevice = nil
isCallStarted = false
}
// The call stopped not by user
func stopCurrentCall() {
guard let intercomDevice = currentIntercomDevice else {
print("Stop current call: No current intercom device")
return
}
callProvider.reportCall(with: intercomDevice.id, endedAt: nil, reason: .answeredElsewhere)
currentIntercomDevice = nil
isCallStarted = false
}
func openDoor() {
guard let intercomDevice = currentIntercomDevice else {
print("Open door: No current intercom device")
return
}
// Completion or didFinishOpening delegate method
intercomManager.open(doorAssociatedWith: intercomDevice, completion: nil)
}
}
SmartSpacesKit’s Intercom.Manager
delegate
extension Intercom.Handler: IntercomManagerDelegate {
func didFinishOpening(intercomManager: Intercom.Manager, device intercomDevice: Intercom.Device, error: Error?) {
// Handle the callback
}
func didReceiveIncomingCall(intercomManager: Intercom.Manager, device intercomDevice: Intercom.Device) {
let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
currentIntercomDevice = intercomDevice
let update = CXCallUpdate()
update.hasVideo = true
update.localizedCallerName = intercomDevice.name
update.supportsDTMF = false
update.supportsGrouping = false
update.supportsHolding = false
update.supportsUngrouping = false
callProvider.reportNewIncomingCall(with: intercomDevice.id, update: update) { [weak self] error in
if let error = error {
print("Reporting a new call is failed with error: \(error)")
if let incomingCallError = error as? CXError, let errorCode = CXErrorCodeIncomingCallError.Code(rawValue: incomingCallError.errorCode) {
switch errorCode {
case .unknown:
break
case .unentitled:
print("The app isn’t entitled to receive incoming calls")
case .callUUIDAlreadyExists:
print("The incoming call UUID \(intercomDevice.id) already exists")
case .filteredByDoNotDisturb:
print("The incoming call is filtered because Do Not Disturb is active and the incoming caller is not a VIP")
// Handle Do Not Disturb
case .filteredByBlockList:
print("The incoming call is filtered because the incoming caller has been blocked by the user")
default:
break
}
}
self?.intercomManager.report(callStatus: .error, for: intercomDevice) { reportingError in
if let reportingError = reportingError {
print("Reporting the call status \(Intercom.CallStatus.error) failed with error: \(reportingError)")
}
UIApplication.shared.endBackgroundTask(backgroundTask)
}
return
}
UIApplication.shared.endBackgroundTask(backgroundTask)
}
}
func shouldStopCalling(intercomManager: Intercom.Manager, device intercomDevice: Intercom.Device?) {
guard currentIntercomDevice != nil else { return }
stopCurrentCall()
}
}
Handle events from Jitsi
extension Intercom.Handler: JitsiMeetViewDelegate {
func conferenceWillJoin(_ data: [AnyHashable: Any]) {
print("JitsiMeetView: Conference will join: \(String(describing: data))")
}
func conferenceJoined(_ data: [AnyHashable: Any]) {
print("JitsiMeetView: Conference joined: \(String(describing: data))")
}
func conferenceTerminated(_ data: [AnyHashable: Any]) {
print("JitsiMeetView: Conference terminated: \(String(describing: data))")
}
func participantJoined(_ data: [AnyHashable: Any]) {
print("JitsiMeetView: Participant \(String(describing: data)) joined")
}
func participantLeft(_ data: [AnyHashable: Any]) {
print("JitsiMeetView: Participant \(String(describing: data)) left")
}
}
Handling PushKit events
extension Intercom.Handler: PKPushRegistryDelegate {
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
guard type == .voIP else { return }
userDeviceManager.update(voipNotificationToken: pushCredentials.token)
}
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
userDeviceManager.update(voipNotificationToken: Data())
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
let backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
defer {
UIApplication.shared.endBackgroundTask(backgroundTask)
completion()
}
guard type == .voIP else {
print("The push payload type is not VoIP")
return
}
guard SWUserSessionManager.shared().session != nil else {
print("The user is not logged in")
return
}
guard JSONSerialization.isValidJSONObject(payload.dictionaryPayload), let payloadDictionary = payload.dictionaryPayload as? [String: Any] else {
print("VoIP push payload is not valid")
return
}
do {
try userDeviceManager.restoreCurrentUserDevice()
} catch {
print("UserDeviceManager is not restored, error: \(error)")
return
}
do {
try intercomManager.on(pushNotificationPayload: payloadDictionary)
} catch {
print("Intercom.Manager returned an error: \(error)")
}
}
}
Handle CallKit events
extension Intercom.Handler: CXProviderDelegate {
func providerDidReset(_ provider: CXProvider) {
stopAudio()
hideCallScreen()
currentIntercomDevice = nil
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
guard let currentIntercomDevice = currentIntercomDevice else {
print("There is no active intercomDevice, the action \(action.callUUID) failed")
action.fail()
return
}
isCallStarted = true
intercomManager.report(callStatus: .start, for: currentIntercomDevice) { [weak self] error in
guard error == nil else {
print("Reporting the call status \(Intercom.CallStatus.start) failed with error: \(error!)")
action.fail()
return
}
self?.configureAudioSession { succeded in
if succeded {
action.fulfill()
} else {
action.fail()
}
}
}
}
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
guard let currentIntercomDevice = currentIntercomDevice else {
print("There is no active intercomDevice, the action \(action.callUUID) failed")
action.fail()
return
}
if isCallStarted {
stopAudio()
}
intercomManager.report(callStatus: isCallStarted ? .end : .ignore, for: currentIntercomDevice) { error in
if let error = error {
print("Reporting the call status \(Intercom.CallStatus.end) failed with error: \(error)")
}
}
isCallStarted = false
action.fulfill()
}
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
showCallScreen()
startAudio()
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
hideCallScreen()
}
}