CallKit issue with call holding (iOS 10) - ios

I have an issue with CallKit API on iOS 10.3.3 and lower iOS versions.
My configuration supports call holding and grouping, here is the snippet.
let configuration = CXProviderConfiguration(localizedName: applicationName)
configuration.supportsVideo = supportsVideo
configuration.supportedHandleTypes = [.phoneNumber]
configuration.maximumCallGroups = 2
configuration.maximumCallsPerCallGroup = 3
On iOS 10, CallKit drops the call when app requests setHeld action for call.
Here is the snippet of setHeld call action request.
public func performSetCallOnHoldAction(_ call: Call, isOnHold: Bool) {
let setHeldAction = CXSetHeldCallAction(call: UUID(uuidString: call.callUUID)!, onHold: isOnHold)
let transaction = CXTransaction()
transaction.addAction(setHeldAction)
requestTransaction(transaction)
}
And finally, here is the CXProvider delegate method, which completes the hold/unhold action for call:
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
// Retrieve the Call instance corresponding to the action's call UUID
guard let call = CallList.sharedInstance().call(withUUID: action.callUUID.uuidString) else {
action.fail()
return
}
// Perform PJSUA set held action
if action.isOnHold {
Pjsua2Wrapper.sharedInstance()?.holdCall(withCallUUID: call.callUUID, completion: { (error) in
(error != nil) ? action.fail() : action.fulfill()
})
} else {
Pjsua2Wrapper.sharedInstance()?.unholdCall(withCallUUID: call.callUUID, completion: { (error) in
(error != nil) ? action.fail() : action.fulfill()
})
}
}
Note that I use Pjsip to perform a hold/unhold action and inside a completion block I call action.fulfill() or action.fail(), depending wether psjip returns the error.
In application GUI, there is a toggle swap button, which calls function to perform CXSetHeldCallAction on CXCallController.
On iOS 11.0 everything works perfectly, but on iOS 10, when you press that toggle button, the call which is supposed to be set on hold, gets ended by the CXProvider for some reason (CXProvider responds with CXEndCallAction).
I have looked in debugger for requestTransaction method wether it returns transaction error, but there is no error in requesting a transaction to call controller.
Does anyone have an idea what is wrong, or something that I can try out to fix this issue?

Related

How to set timeout for CallKit incoming call UI

I'm developing Video call feature in my app and using CallKit to be the incoming call UI. And i found an edge case like:
User A: call user B
User B:
The app is in terminated state. And CallKit incoming UI shows for user B.
User B doesn't notice (since silent mode) and let the incoming UI keep showing
User A doesn't end the call; or for some reasons, user A lost internet or quit the app (therefore my server doesn't send Cancel command via VoIP notification) so there's no way for user B to end the incoming UI until user B touch Cancel or Answer
So is there any way to set a timeout for an incoming UI of CallKit? For example: If i set the timeout is 60 seconds then the incoming UI just shows in 60 seconds the auto dismiss.
Here is my code to show the incoming UI:
let update = CXCallUpdate()
update.localizedCallerName = callerName
update.remoteHandle = CXHandle(type: .phoneNumber, value: myID)
update.hasVideo = true
self.provider.reportNewIncomingCall(with: uuid, update: update) { [weak self] error in
guard let self = self else { return }
if error == nil {
// Store my calls
let call = Call(uuid: uuid, handle: handle)
self.callKitManager.add(call: call)
}
}
Any help would be greatly appreciated. Thanks.
There's no way to set a timeout using the CallKit API. You just have to implement it by yourself.
In the class where you handle all the call logic, you should add something like the following:
private func startRingingTimer(for call: Call)
{
let vTimer = Timer(
timeInterval: 60,
repeats: false,
block: { [weak self] _ in
self?.ringingDidTimeout(for: call)
})
vTimer.tolerance = 0.5
RunLoop.current.add(vTimer, forMode: .common)
ringingTimer = vTimer
}
private func ringingDidTimeout(for call: Call)
{
...
self.provider.reportCall(with: call.uuid, endedAt: nil, reason: .unanswered)
...
}
Then you should call startRingingTimer(for: call) as soon as you successfully reported a new incoming call; and, of course, you have to invalidate the timer if the user answers the call.

CallKit UI for reportNewIncomingCall is not showing when the user disconnect by clicking on "remind me" and a different call come

Sometimes the CallKit UI is not visible.
This happens all the time when the user clicks on the "Remind me" button on CallKit UI and cancel the call.
Now, when the user gets the call for the second time, there is only vibration but no UI for CallKit.
let callHandle = CXHandle(type: .generic, value: callerName ?? "Unknown".localized)
let callUpdate = getCallUpdate(callHandle: callHandle)
print("reportNewIncomingCall uuid = \(uuid)")
callKitProvider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
if let error = error {
NSLog("Failed to report incoming call successfully: \(error.localizedDescription).")
} else {
NSLog("Incoming call successfully reported.")
}
completion?(error as NSError?)
}
I am also faced this issue and came to know that I have used same uuid for all calls, after changing it to below it worked for me .so make sure to generate new uuid for each call.
provider.reportNewIncomingCall(with: UUID(), update: update) { error in
if let error = error{
print("error while reporting incoming call \(error)")
}else{
print("incoming call successfully reported")
}
completion?(error)
}

How do I end the call session on callkit from my custom ongoing call UI?

When a user end a call from the CallKit UI the app ends the call and the actual VOIP call also end. But when I end the call from my custom UI the VOIP call ends but the CallKit is still active. How do I end the CallKit session from my custom UI?
This is what happens when I press end call on the CallKit UI:
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
XCPjsua.shared()?.endCall()
action.fulfill()
}
This is what happens when I end call from my custom UI (Should I close CallKit here?):
- (void)endcall {
[[XCPjsua sharedXCPjsua] endCall];
}
If you want to end the call from your custom UI you should do that through a CXTransaction:
let callController = CXCallController()
let endCallAction = CXEndCallAction(call: aUUID)
callController.request(
CXTransaction(action: endCallAction),
completion: { error in
if let error = error {
print("Error: \(error)")
} else {
print("Success")
}
})
this will cause provider(_ provider: CXProvider, perform action: CXEndCallAction) to be called.
In all other cases (i.e. remote ended, unanswered, etc... - see CXCallEndedReason) you should only report the ended call:
let provider: CXProvider
provider.reportCall(with: call.uuid, endedAt: Date(), reason: .remoteEnded)
in this case provider(_ provider: CXProvider, perform action: CXEndCallAction) will not be called.
I use this code to close call
provider?.reportCall(with: PushUtility.shared.uuid!, endedAt: Date(), reason: .remoteEnded)
First save your uuid which you are using for connecting call use that uuid for end call.
I managed to close it using the reportCall function
provider?.reportCall(with: appG.uuid, endedAt: Date(), reason: .remoteEnded)
So I just call that function when I press end call from my custom UI

FaceID/TouchID success case keeps prompting for further authentication

I've implemented password/TouchID/FaceID on a view controller and when I hit the success case, I'd expect the prompt to stop firing but it just fires over and over again.
In my VC:
var context: LAContext!
func authenticateReturningUser() {
context = LAContext()
var error: NSError?
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
let reason = "Verify that this is your device to continue."
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, error in
DispatchQueue.main.sync {
guard success else {
guard let error = error else {
// show error
return
}
switch error {
case LAError.userCancel:
// do stuff
return
default: return
}
}
print("success")
}
}
}
}
The prompt should fire once and not again if the user successfully authorizes
Edit:
authenticateReturningUser is called from the AppDelegate's applicationDidBecomeActive function:
self.coverVC?.completionHandler = { self.removeBackgroundVC() }
self.coverVC?.authenticateReturningUser()
As far as I remember, when showing the Touch ID prompt, your app becomes inactive. So when the prompt is dismissed, your app becomes active again, triggering the App Delegate's applicationDidBecomeActive again.
You might consider introducing a flag that stores whether the app became inactive because of Touch ID / Face ID etc. or because of another reason and use it in applicationDidBecomeActive to decide if authentication should be triggered or not.
Where are you calling authenticateReturningUser()? You may want to create a static boolean authenticated that if false, allows the call to authenticateReturningUser(), and if true, skips the call, and set authenticated = true after calling the function once.

Parse.com PFUser signUpInBackgroundWithBlock: block not being called on first tap

i am developing an iOS-App with Swift and the parse.com framework and have a big problem with registering new users.
The block of "signUpInBackgroundWithBlock" is not being called on the first tap, although the new user is getting registered. When i am tapping the button a second time, the block gets finally called and i get an error, that the username is already registered.
var newUser = PFUser()
newUser.username = registerView.nicknameTextField.text.trim()
newUser.email = registerView.emailTextField.text
newUser.password = registerView.passwordTextField.text
newUser.signUpInBackgroundWithBlock {
(succeeded: Bool, error: NSError!) -> Void in
self.registerCompletionBlock(succeeded, error: error)
}
Is someone having the same problem and knows a solution for this strange behaviour?
Thanks!
Edit:
The completion block should call the "registerCompletionBlock()" function:
func registerCompletionBlock(succeeded: Bool, error: NSError!) {
if error == nil {
let subscriptionStoryboard = UIStoryboard(name: "Subscription", bundle: nil)
let viewcontroller: UIViewController = subscriptionStoryboard.instantiateInitialViewController() as UIViewController
self.presentViewController(viewcontroller, animated: true, completion: nil)
} else {
if let errorString = error.userInfo?["error"] as? NSString {
println(errorString)
if error.userInfo?["code"] as Float == 202{
let alert = UIAlertView(title: "vergeben", message: "name vergeben", delegate: nil, cancelButtonTitle: "abbrechen")
alert.show()
}
}
}
}
I tried the solution posted previously (eliminating PFUser.enableAutomaticUser()), and the issue persisted.
In case anyone else is still looking for a solution to this issue, try changing the if error == nil to if succeeded == true in the block of signUpInBackground. This worked for me and all is functional in backend.
Its calling at first time, but its taking a little time for calling this .. because its sending data on server in asynchronous. Asynchronous never block the main thread..
Because : -
Asynchronous never block the main thread waiting for a network response.
Asynchronous can be either synchronous on a separate thread, or scheduled in the run loop of any thread.
Asynchronous can be either synchronous on a separate thread, or scheduled in the run loop of any thread.
Synchronous blocks main thread until they complete request.
Because you call the method asynchronous, it takes some time to do it and your main thread doesn't wait for the method to finish. So you should, if you want to show a message or perform a segue, after the registration, put it inside the completion-block:
newUser.signUpInBackgroundWithBlock {
(succeeded: Bool!, error: NSError!) -> Void in
if error == nil {
// Perform a segue, show a message or whatever you want
} else {
let errorString = error.userInfo["error"] as NSString
// Show the errorString somewhere and let the user try again.
}
}
Also if you don't want to do it asynchronous, you can call the signUp() method(without the inBackgroundWithBlock. That way the application waits for the signup to finish until it continues.
i figured it out. Problem was, that i used PFUser.enableAutomaticUser() in the AppDelegate. I removed that line and the signup-block works flawlessly now!

Resources