Trigger an incoming VoIP call using CallKit and Twilio-Video API - ios

By using one of the sample video calling app provided by Twilio (VideoCallKitQuickStart), I am trying to trigger an incoming call by sending a VoIP notification to the App. But the App doesn't trigger an incoming call. I also tried keeping the App opened while sending a VoIP notification and the App crashes, by throwing the below exception
NSInvalidArgumentException: Attempt to
insert non-property list object 'PKPushPayload: 0x16e44af0' for key
payload
Could someone, please help me or point me in the right direction on how to trigger an incoming call in the App, when a VoIP notification is received.
Below is my code in the ViewController.swift file
func pushRegistry(registry: PKPushRegistry!, didReceiveIncomingPushWithPayload payload: PKPushPayload!, forType type: String!) {
// Process the received push
self.reportIncomingCall(uuid: UUID(), roomName: "testRoom", completion: nil)
}
func reportIncomingCall(uuid: UUID, roomName: String?, completion: ((NSError?) -> Void)? = nil) {
let callHandle = CXHandle(type: .generic, value: roomName ?? "")
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = callHandle
callUpdate.supportsDTMF = false
callUpdate.supportsHolding = true
callUpdate.supportsGrouping = false
callUpdate.supportsUngrouping = false
callUpdate.hasVideo = true
callKitProvider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
if error == nil {
NSLog("Incoming call successfully reported.")
} else {
NSLog("Failed to report incoming call successfully: \(error?.localizedDescription).")
}
completion?(error as? NSError)
}
}

Posting late answer but it may helpful for someone.
Following code I did to handle voice incoming call.
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) {
NSLog("pushRegistry:didReceiveIncomingPushWithPayload:forType:")
print(payload)
if (type == PKPushType.voIP) {
TwilioVoice.handleNotification(payload.dictionaryPayload, delegate: self)
pushKitPushReceivedWithPayload(payload: payload)
}
}
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: #escaping () -> Void) {
NSLog("pushRegistry:didReceiveIncomingPushWithPayload:forType:completion:")
if (type == PKPushType.voIP) {
TwilioVoice.handleNotification(payload.dictionaryPayload, delegate: self)
pushKitPushReceivedWithPayload(payload: payload)
}
completion()
}
func pushKitPushReceivedWithPayload(payload: PKPushPayload){
if UIApplication.shared.applicationState != .active{
let msgType = payload.dictionaryPayload["twi_message_type"] as? String
if let messageType = msgType{
if messageType == "twilio.voice.call"{
fireLocalNotificationForVoiceCall(didStart: true)
}else if messageType == "twilio.voice.cancel"{
fireLocalNotificationForVoiceCall(didStart: false)
}
}
}
}
Below are the delegate methods of call kit I have added
extension AppDelegate : TVONotificationDelegate, TVOCallDelegate
{
func callInviteReceived(_ callInvite: TVOCallInvite)
{
if (callInvite.state == .pending)
{
//code
}
else if (callInvite.state == .canceled)
{
//code
}
}
func handleCallInviteReceived(_ callInvite: TVOCallInvite)
{
//code
}
func handleCallInviteCanceled(_ callInvite: TVOCallInvite)
{
//code
}
}
I have followed this tutorial provided by twilio - https://github.com/twilio/voice-quickstart-swift
Go through this tutorial and it will work.

Twilio developer evangelist here.
I'm not particularly good with iOS, but taking a quick look at the documentation for the PKPushRegistryDelegate it looks like your pushRegistry function definition isn't right.
It should be
func pushRegistry(_ registry: PKPushRegistry,
didReceiveIncomingPushWith payload: PKPushPayload,
forType type: PKPushType)
That is, didReceiveIncomingPushWith rather than didReceiveIncomingPushWithPayload.
Alternatively, does it have anything to do with the fact that you're casting forType to String?

Swift 3.0
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType) {
NSLog("pushRegistry:didReceiveIncomingPushWithPayload:forType:")
if (type == PKPushType.voIP) {
print(payload.dictionaryPayload)
VoiceClient.sharedInstance().handleNotification(payload.dictionaryPayload, delegate: self)
}
}
And please don't make any changes in payload without modifying it in order for the SDK to extract the incoming call info out of the payload so that the SDK can notify the application with incoming calls

Related

Voip call got crashed when app is background and triggered endCall method. (After adding timer for unanswered case)

I'm trying to implement unanswered case for voip call.
When inside reportNewIncomingCall's completion I started internal timer to track timeout for 60sec.
public final class CallCenter: NSObject {
fileprivate var sessionPool = [UUID: String]()
public func showIncomingCall(of session: String, completion: #escaping () -> Void) {
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = CXHandle(type: .generic, value: session)
callUpdate.localizedCallerName = session
callUpdate.hasVideo = true
callUpdate.supportsDTMF = false
let uuid = pairedUUID(of: session)
provider.reportNewIncomingCall(with: uuid, update: callUpdate, completion: { [unowned self] error in
if let error = error {
print("reportNewIncomingCall error: \(error.localizedDescription)")
}
// We cant auto dismiss incoming call since there is a chance to get another voip push for cancelling the call screen ("reject") from server.
let timer = Timer(timeInterval: incomingCallTimeoutDuration, repeats: false, block: { [unowned self] timer in
self.endCall(of: session, at: nil, reason: .unanswered)
self.ringingTimer?.invalidate()
self.ringingTimer = nil
})
timer.tolerance = 0.5
RunLoop.main.add(timer, forMode: .common)
ringingTimer = timer
completion()
})
}
public func endCall(of session: String, at: Date?, reason: CallEndReason) {
let uuid = pairedUUID(of: session)
provider.reportCall(with: uuid, endedAt: at, reason: reason.reason)
}
}
When peer user (caller) declined, I will get another voip notification and i'm calling this.
callCenter.endCall(of: caller, at: Date(), reason: .declinedElsewhere)
Scenario:
The incoming call is shows when app is in foreground.
User do nothing and call was cancelled (the timer got triggered.)
User minimised the app (app is in background), and then received new voip call update. The app got crashed with message
terminating with uncaught exception of type NSException
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Killing app because it
never posted an incoming call to the system after receiving a PushKit
VoIP push.'
Appdelegate:
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: #escaping () -> Void) {
print("Payload: \(payload.dictionaryPayload)")
guard let data = payload.dictionaryPayload as? [String: Any],
let userID = data["user"] as? UInt64,
let event = data["event"] as? String,
let caller = data["callerName"] as? String
else {
print("Incoming call failed due to missing keys.")
callCenter.showIncomingCall(of: "Unknown") { [unowned self] in
self.callCenter.endCall(of: "Unknown", at: nil, reason: .failed)
completion()
}
return
}
switch event {
case "reject":
callCenter.endCall(of: caller, at: Date(), reason: .declinedElsewhere)
callingUser = nil
completion()
return;
case "cancel":
callCenter.endCall(of: caller, at: Date(), reason: .answeredElsewhere)
callingUser = nil
completion()
return;
default: break
}
let callingUser = CallingUser(session: caller, userID: userID)
callCenter.showIncomingCall(of: callingUser.session) {
completion()
}
self.callingUser = callingUser
}
Above scenario works well without unanswered case. Means, i can able to trigger endCall method (with any reason) when app is in background. And it works.
So i think issue is with the timer.
Basically I'm calling endCall method with same UUID and for different reasons. And its works fine if I remove timer logic.
What's best practice or recommended way to implement unanswered case.? Where did I go wrong?
I can above to resolve this issue by initiating a fake call if there is no Active calls in the app.
private func showFakeCall(of session: String, callerName: String, completion: #escaping () -> Void) {
callCenter.showIncomingCall(of: session, callerName: callerName) { [unowned self] in
self.callCenter.endCall(of: session, at: nil, reason: .failed)
print("Print end call inside Fakecall")
completion()
}
}
Added following check for all of the call events (reject, cancel)
if !callCenter.hasActiveCall(of: channelName) {
print("No Active calls found. Initiating Fake call.")
showFakeCall(of: channelName, callerName: callerName, completion: completion)
return
}
Extra tip: You have to reinstall (uninstall first), if you made any changes to CXProvider/CXCallController configurations.

Voip notification did not receive when app kill or terminated in iOS 14

in my app,
it's the existing functionality of VoIP calls. some user report to me that don't receive VoIP calls. after I check so found that the reported user version is ios 13 or later.
but one my friend have iPhone 6 plus (ios 12.5) in that I received VoIP calls.
there are any settings that remain for ios 13 or later.
here is my code:
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: #escaping () -> Void) {
let provider1 = CXProvider(configuration: defaultConfig())
provider1.setDelegate(self, queue: nil)
let update = CXCallUpdate()
update.supportsDTMF = true
update.supportsHolding = true
update.supportsGrouping = false
update.supportsUngrouping = false
update.hasVideo = false
// self.provider = provider1
let bgTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
UIApplication.shared.endBackgroundTask(bgTaskID)
}
uuidneww = UUID()
update.remoteHandle = CXHandle(type: .generic, value: "Calling")
provider1.reportNewIncomingCall(with:uuidneww , update: update, completion: { error in })
}
iOS 15:
If there's a push in foreground/background but there's no push in terminated state, and you fire CallKit API's be sure that:
PKPushRegistry object is created in application(didFinishLaunchingWithOptions:). This step is very important, coz you may create it within some view controller and of course it won't be called in background.
delegate is set
pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler: delegate method is called. Be sure to use the method with completion block, thus it still works it may not in the future

IOS app crashing when recieving pushkit voip notification after force close

I'm trying to make pushkit voip messages work when the application is closed. The calls work and get displayed when app is in the foreground or in the background. But after the user force kills the app, when the notification gets recieved, the app terminates with signal 9 (killed by user/ios).
How can I fix this issue?
I've got background fetch, voip, audio and push notifications enabled in my app.
Also tried removing all the Unity methods, putting the Callkit call in the PushRegistry method, creating a new provider when recieving a notification, even subscribing to the UIApplicationDidFinishLaunchingNotification event, but nothing worked.
I've made it so the app is compliant to showing a call when recieving a voip notification. Here's my code:
#objcMembers class CallPlugin: UIResponder, UIApplicationDelegate, PKPushRegistryDelegate, CXProviderDelegate {
static var Instance: CallPlugin!
var provider: CXProvider!
var registry:PKPushRegistry!
var uuid:UUID!
var callController: CXCallController!
//class entry point
public static func registerVoIPPush(_ message: String) {
Instance = CallPlugin()
//Pushkit
Instance.registry = PKPushRegistry(queue: DispatchQueue.main)
Instance.registry.delegate = Instance
Instance.registry.desiredPushTypes = [PKPushType.voIP]
//Callkit
let providerConfiguration = CXProviderConfiguration(localizedName: "testing")
providerConfiguration.supportsVideo = true
providerConfiguration.supportedHandleTypes = [.generic]
Instance.provider = CXProvider(configuration: providerConfiguration)
Instance.provider.setDelegate(Instance, queue: nil)
UnitySendMessage("ResponseHandler", "LogNative", "registration success")
}
//Get token
func pushRegistry( _ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
if type == PKPushType.voIP {
let deviceTokenString = credentials.token.map { String(format: "%02.2hhx", $0) }.joined()
UnitySendMessage("ResponseHandler", "CredentialsRecieved",deviceTokenString)
}
}
//Get notification
func pushRegistry( _ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type:PKPushType, completion: #escaping () -> Void) {
//UnitySendMessage("ResponseHandler", "LogNative", "Got something push")
reportInComingCallWith(uuidString: "111", handle: "Paul", isVideo: false)
completion()
}
//show the call
func reportInComingCallWith(uuidString:String,handle:String,isVideo:Bool) {
//UnitySendMessage("ResponseHandler", "LogNative", "attempting call")
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = CXHandle(type: .generic, value: handle)
callUpdate.hasVideo = false
uuid = NSUUID() as UUID
provider.reportNewIncomingCall(with: uuid as UUID, update: callUpdate){ (error) in
if let error = error {
UnitySendMessage("ResponseHandler", "LogNative", "error in starting call"+error.localizedDescription)
}
}
}
The issue was that my app was not creating the delegate and pushregistry objects in time for the notification to get fully handled.
I solved it by overriding the app delegate and putting the notification initializations in WillFinishLaunching, like so:
-(BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(nullable NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions{
[CallPlugin registerVoIPPush:#"hmm"];
[super application:application willFinishLaunchingWithOptions:launchOptions];
return true;
}
That way everything is ready for the notification to be handled. I tried to put this in DidFinishLaunching first, but it was already too late for the notification and IOS was killing my application by then.

How to stream data from swift to flutter using event channel?

I am making an app in which I stream data from native android and iOS side to flutter side and there I display the data in the UI.
I already did the android part. The android part is sending the data to flutter side and displays them in UI. But the problem is how to achieve same for iOS swift side.
Android code that works for me:
new EventChannel(getFlutterView(), "Eventchannelname").setStreamHandler(
new EventChannel.StreamHandler() {
#Override
public void onListen(Object args, EventChannel.EventSink events) {
Log.w(TAG, "adding listener");
mEventSink = events; // I use mEventsink.success(data) to pass the data to flutter side
#Override
public void onCancel(Object args) {
Log.w(TAG, "cancelling listener");
}
}
);
How can I achieve the same in Swift native code. I googled it and did not find anything that can help me.
I want same in swift as what I did in android java: I want to capture the events in local variable and then use that where I need to send data to flutter.
just call mEventSink as a function
mEventSink(data)
Use FlutterEndOfEventStream constant to signal end of stream
mEventSink(FlutterEndOfEventStream)
if you going to send error to flutter side use
mEventSink(FlutterError(code: "ERROR_CODE",
message: "Detailed message",
details: nil))
Reference to API DOC
Complete swift example
let eventChannel = FlutterEventChannel(name: "your.channel.id", binaryMessenger: messenger!)
eventChannel.setStreamHandler(SwiftStreamHandler())
....
class SwiftStreamHandler: NSObject, FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: #escaping FlutterEventSink) -> FlutterError? {
events(true) // any generic type or more compex dictionary of [String:Any]
events(FlutterError(code: "ERROR_CODE",
message: "Detailed message",
details: nil)) // in case of errors
events(FlutterEndOfEventStream) // when stream is over
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
return nil
}
}
I also find an example in the official Flutter repo: https://github.com/flutter/flutter/blob/master/examples/platform_channel_swift
The code in Swift looks like the following:
import UIKit
import Flutter
enum ChannelName {
static let battery = "samples.flutter.io/battery"
static let charging = "samples.flutter.io/charging"
}
enum BatteryState {
static let charging = "charging"
static let discharging = "discharging"
}
enum MyFlutterErrorCode {
static let unavailable = "UNAVAILABLE"
}
#UIApplicationMain
#objc class AppDelegate: FlutterAppDelegate, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
GeneratedPluginRegistrant.register(with: self)
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController is not type FlutterViewController")
}
let batteryChannel = FlutterMethodChannel(name: ChannelName.battery,
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
[weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented)
return
}
self?.receiveBatteryLevel(result: result)
})
let chargingChannel = FlutterEventChannel(name: ChannelName.charging,
binaryMessenger: controller.binaryMessenger)
chargingChannel.setStreamHandler(self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func receiveBatteryLevel(result: FlutterResult) {
let device = UIDevice.current
device.isBatteryMonitoringEnabled = true
guard device.batteryState != .unknown else {
result(FlutterError(code: MyFlutterErrorCode.unavailable,
message: "Battery info unavailable",
details: nil))
return
}
result(Int(device.batteryLevel * 100))
}
public func onListen(withArguments arguments: Any?,
eventSink: #escaping FlutterEventSink) -> FlutterError? {
self.eventSink = eventSink
UIDevice.current.isBatteryMonitoringEnabled = true
sendBatteryStateEvent()
NotificationCenter.default.addObserver(
self,
selector: #selector(AppDelegate.onBatteryStateDidChange),
name: UIDevice.batteryStateDidChangeNotification,
object: nil)
return nil
}
#objc private func onBatteryStateDidChange(notification: NSNotification) {
sendBatteryStateEvent()
}
private func sendBatteryStateEvent() {
guard let eventSink = eventSink else {
return
}
switch UIDevice.current.batteryState {
case .full:
eventSink(BatteryState.charging)
case .charging:
eventSink(BatteryState.charging)
case .unplugged:
eventSink(BatteryState.discharging)
default:
eventSink(FlutterError(code: MyFlutterErrorCode.unavailable,
message: "Charging status unavailable",
details: nil))
}
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
NotificationCenter.default.removeObserver(self)
eventSink = nil
return nil
}
}

What's the most efficient way to refresh a tableView every time the app is pushed back to the foreground?

Currently what I have is this:
AppDelegate.applicationDidBecomeActive():
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
guard let vc = self.window?.rootViewController?.children.first as! AlarmTableViewController? else {
fatalError("Could not downcast rootViewController to type AlarmTableViewController, exiting")
}
vc.deleteOldAlarms(completionHandler: { () -> Void in
vc.tableView.reloadData()
})
}
deleteOldAlarms():
func deleteOldAlarms(completionHandler: #escaping () -> Void) {
os_log("deleteOldAlarms() called", log: OSLog.default, type: .default)
let notificationCenter = UNUserNotificationCenter.current()
var activeNotificationUuids = [String]()
var alarmsToDelete = [AlarmMO]()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
notificationCenter.getPendingNotificationRequests(completionHandler: { (requests) in
for request in requests {
activeNotificationUuids.append(request.identifier)
}
for alarm in self.alarms {
guard let alarmUuids = alarm.value(forKey: "notificationUuids") as! [String]? else {
os_log("Found nil when attempting to unwrap notificationUuids in deleteOldAlarms() in AlarmTableViewController.swift, cancelling",
log: OSLog.default, type: .default)
return
}
let activeNotificationUuidsSet: Set<String> = Set(activeNotificationUuids)
let alarmUuidsSet: Set<String> = Set(alarmUuids)
let union = activeNotificationUuidsSet.intersection(alarmUuidsSet)
if union.isEmpty {
alarmsToDelete.append(alarm)
}
}
os_log("Deleting %d alarms", log: OSLog.default, type: .debug, alarmsToDelete.count)
for alarmMOToDelete in alarmsToDelete {
self.removeNotifications(notificationUuids: alarmMOToDelete.notificationUuids as [String])
managedContext.delete(alarmMOToDelete)
self.alarms.removeAll { (alarmMO) -> Bool in
return alarmMOToDelete == alarmMO
}
}
completionHandler()
})
}
but it feels disgusting. Plus, I'm calling tableView.reloadData() on a background thread now (the thread executing the completion handler). What's the best way to refresh the UI once the user opens the app back up? What I'm aiming for is for these old alarms to be deleted and for the view to be reloaded. An alarm is considered old if it doesn't have any notifications pending in the notification center (meaning the notification has already been executed).
Don't put any code in the app delegate. Have the view controller register to receive notifications when the app enters the foreground.
Add this in viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(enteringForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
Then add:
#objc func enteringForeground() {
deleteOldAlarms {
DispatchQueue.main.async {
tableView.reloadData()
}
}
}
As of iOS 13, you should register for UIScene.willEnterForegroundNotification. If your app needs to work under iOS 13 as well as iOS 12 then you need to register for both notifications but you can use the same selector.
You can use NSNotification
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
In didBecomeActive call tableView.reloadData(), that should be all. You should remember to unregister the observer in deinit.
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)

Resources