CallKit screen goes behind the app UI while ringing - ios

We have an iOS app that is configured to receive VoIP notifications through the PushKit framework. When a VoIP notification arrives, we immediately report the incoming call to CallKit (as per iOS 13 guidelines) with the following code:
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: #escaping () -> Void) {
let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
self.callsManager.handleIncomingNotification(with: payload.dictionaryPayload, backgroundTaskIdentifier: backgroundTaskIdentifier, completion: completion)
}
CallsManager is a singleton class that handles the incoming notification as follows:
let call = addOrUpdateCall(
service: TwilioService(),
token: voipNotification.token,
in: voipNotification.room,
with: voipNotification.nickname,
uuid: UUID())
if call.ring() != .none {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .emailAddress, value: voipNotification.email)
update.hasVideo = voipNotification.hasVideo
update.supportsDTMF = false
update.supportsHolding = false
update.supportsGrouping = false
update.supportsUngrouping = false
update.localizedCallerName = voipNotification.nickname
self.callProvider?.reportNewIncomingCall(with: call.uuid, update: update, completion: { (error) in
completion()
if let error = error {
self.serialQueue.async {
switch error {
case CXErrorCodeIncomingCallError.filteredByDoNotDisturb, CXErrorCodeIncomingCallError.filteredByBlockList:
_ = call.decline()
default:
call.fail(error: error)
}
}
}
})
}
Basically, we are extracting the necessary info from the notification and we're immediately displaying the incoming call screen.
The addOrUpdateCall function just makes sure that in the array of calls that we have in memory, we are not storing duplicate calls.
Normally, this works perfectly: the CallKit incoming call screen is displayed and the phone starts ringing. But sometimes, in a very sporadic way, the CallKit screen is displayed for a second and then disappears behind the app UI.
This only happens when the app is in foreground or the phone is unlocked, it does not happen if the phone is locked.
After many many attempts, we've found a somewhat systematic way of reproducing the issue using two iOS devices:
From one device, call the other one and hang up after a few seconds.
Repeat this three times in a row, leaving a few seconds between each call.
Call from the other device: on the first device, the CallKit UI is displayed for a second and then hidden behind the app, but the device keeps ringing and vibrating.
We don't have any log from iOS and the phone keeps ringing and vibrating while the app UI is in front. The only way to restore it is by entering in the multi-tasking and re-selecting the app.
I've already tried to look for a workaround, where I continuously call reportNewIncomingCall every second, hoping that this would bring the CallKit UI in front. It seems to alleviate the issue, but it still happens every once in a while.
We've tried on multiple iPhone models and it seems that the iPhone XS and the iPhone XS Max are not affected (the CallKit UI appears and hides after a second, but it comes back automatically, even without the workaround above), but we were able to reproduce the issue on an iPhone 7, two iPhone 6S and an iPhone 6S Plus.
Can anybody help me?

Related

How to end a CallKit call from custom UI without having caller UUID?

I working on a video call application in iOS Swift 5, where call can only initiate from the backend web app and the mobile app can only answer to those calls,. Means there is no mobile-mobile communication, communication between web app and mobile app. In my application I'm using PushKit with CallKit for notifying the incoming call in the background or killed state. So in the background or killed state, if I get an incoming call, it will show the calling screen using CallKit and if I pressed Answer button, it will navigate to my own custom video call screen where I have end button for dismissing the call. But when I press the end button, the VoIP call get disconnected. But the CallKit call is not dismissing(Still show the green bar on the top of Homescreen). And I checked for how ending the CallKit call via code, but in most of the solution the CallKit is dismissing with the use of callUUID. But I don't know from where I will get that UUID. In some code I saw the UUID is received from the push payload, but in my case the call is initiated from the web app, so I'm not receiving the caller UUID. Please help me.
This is my code,
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: #escaping () -> Void) {
print(payload.dictionaryPayload)
let state = UIApplication.shared.applicationState
if state == .background || state == .inactive {
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: callerName)
update.hasVideo = true
provider.reportNewIncomingCall(with: UUID(), update: update, completion: { error in })
} else if state == .active {
// no need for calling screen
}
}
And I tired the following code to end the call, but not working.
func endCall(call: UUID) {
let callController = CXCallController()
let endCallAction = CXEndCallAction(call: call)
let transaction = CXTransaction(action: endCallAction)
callController.request(transaction) { error in
if let error = error {
print("EndCallAction transaction request failed: \(error.localizedDescription).")
self.provider.reportCall(with: call, endedAt: Date(), reason: .remoteEnded)
return
}
print("EndCallAction transaction request successful")
}
Here I'm passing call as current UUID, then I'm getting error response as
EndCallAction transaction request failed: The operation couldn’t be
completed.
The uuid value is one that you provide.
You are providing it here:
provider.reportNewIncomingCall(with: UUID(),...
Because you are simply allocating a new UUID and passing it directly in reportNewIncomingCall you don't know the uuid when you need it later.
You need to store this uuid in a property so that you can provide it to your endCall function.

iOS 13 Incoming Call UI goes to Recents

I'm developing VoIP based audio call in my application. I have a strange issue for which I couldn't find a solution.
For iOS 13+ devices, sometimes incoming CallKit UI goes in Background. That means incoming CallKit UI doesn't show upfront, but I'm able to hear the call ringtone audio and vibration. When I double-tap on the Home button, I'm able to see my app with the IncomingCall UI in Recents. When I tap on it, it shows the CallKit UI and then I'm not able to move to other applications via double-tapping the home button.
It's inconsistently happening on iOS 13+ versions. Is there any way to show CallKit UI prominently while receiving incoming calls?
I'm using the method below to show an incoming call.
let update = CXCallUpdate()
update.remoteHandle = CXHandle(type: .generic, value: callObj.name ?? "")
update.hasVideo = false
provider.reportNewIncomingCall(with: newUUID, update: update) { error in
if error == nil {
let call = Call(uuid: newUUID, handle: callObj.name ?? "", roomID: self.callObj?.roomId ?? "")
self.callManager.add(call: call)
}
completion?(error as NSError?)
}

Why doesn't my iOS (Swift) app properly recognize some external display devices?

So I have an odd issue and my google-fu utterly fails to even provide me the basis of where to start investigating, so even useful keywords to search on may be of use.
I have an iOS application written in swift. I have a model hooked up to receive notifications about external displays. On some adaptors, I'm able to properly detect and respond to the presence of an external display and programatically switch it out to be something other than a mirror (see code block below). But with another adaptor, instead of just 'magically' becoming a second screen, I'm asked to 'trust' the external device, and it simply mirrors the device screen. Not the intended design at all.
func addSecondScreen(screen: UIScreen){
self.externalWindow = UIWindow.init(frame: screen.bounds)
self.externalWindow!.screen = screen
self.externalWindow!.rootViewController = self.externalVC
self.externalWindow!.isHidden = false;
}
#objc func handleScreenDidConnectNotification( _ notification: NSNotification){
let newScreen = notification.object as! UIScreen
if(self.externalWindow == nil){
addSecondScreen(screen: newScreen)
}
}
#objc func handleScreenDidDisconnectNotification( _ notification: NSNotification){
if let externalWindow = self.externalWindow{
externalWindow.isHidden = true
self.externalWindow = nil
}
}
The worst issue here is that because I'm connecting to an external display to do this, I can't even run this code through the debugger to find out what is going on. I don't know where to even begin.
Any ideas?
Edit:
Thanks to someone pointing out wifi debugging, I can tell you my notifications are firing off, but they're both firing at the same time, one after the other, when the external adaptor is disconnected.

WCSession transferUserInfo only works in Foreground

I am using WCSession's tranferUserInfo to send data between the watch and the iOS app for info that needs to be handled when either product is in the background. This works 100% of the time on the simulator but never with actual devices.
By using breakpoints I have discovered that func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) is never called in the background but is immediately called when the app is brought to the foreground. Clearly session.transferUserInfo(data) is being called but not received in the background state. Again running the exact same code but on the simulator works perfectly.
I am running iOS 9.3.2 and Watch OS 2.2.1. Clearly this function was meant to handle communications in the background state and thus I believe the simulator is working as intended. I tried wrapping both the sender and receiver in a dispatch_async(dispatch_get_main_queue(), { block, but to no avail.
What am I missing about transferUerInfo and its seeming inability to work properly with background states?
FYI - breakpoint set at the beginning of didRecieveUserInfo is never hit until the app is brought into the foreground.
func transferInfo(data:[String: AnyObject])
{
dispatch_async(dispatch_get_main_queue(), {
if #available(watchOSApplicationExtension 2.2, *)
{
if #available(iOS 9.3, *)
{
if self.session.activationState == .Activated
{
self.session.transferUserInfo(data)
}
else
{
NSNotificationCenter.defaultCenter().postNotificationName("alertError", object: self, userInfo: ["error":"Failed to transfer"])
}
}
else
{
self.session.transferUserInfo(data)
}
}
else
{
self.session.transferUserInfo(data)
}
})
}
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject])
{
dispatch_async(dispatch_get_main_queue(), {
for delegate in self.watchCommsProtocols
{
delegate.watchCommsDidUpdateInfo!(userInfo)
}
})
}
I just watched the Watch Connectivity session from WWDC 2015. It seems that transferUserInfo cannot be received by iOS until the app is in the foreground. That is of course what I am seeing with actual devices. The issue here then, and what has thrown me off, is that the simulator as of this writing DOES receive these messages when in the background. This is not the correct behavior and should therefore be considered a bug in the functioning of the simulator.
For my purposes I should be able to use sendMessagefrom the watch to iOS when iOS is in the background. However, the same is not true in reverse. To use sendMessage from iOS to the watch, the watch will have to be in the foreground.
Both sides can send while the sending app is in background.
This means
if you are sending while there is no connection,
then your sending app goes into background or stops,
then there is a connection
-> the sending OS will send.
An app in watchOS 2 can't do anything in background. So it can't receive.
On iOS, an app can't bring itself to the foreground while in background. So dispatch_async(dispatch_get_main_queue() doesn't make sense here.

Apple Watch Background Mode?

I am developing apple watch application. when i run the app it is working fine. Now my problem is when the app goes to background mode, the app on the apple watch app will closing automatically. I am writing small code in iPhone app:
func viewDidLoad() {
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
// In your WatchKit extension, the value of this property is true when the paired iPhone is reachable via Bluetooth.
// On iOS, the value is true when the paired Apple Watch is reachable via Bluetooth and the associated Watch app is running in the foreground.
// In all other cases, the value is false.
if session.reachable {
lblStatus.text = "Reachable"
}
else
{
lblStatus.text = "Not Reachable"
}
func sessionReachabilityDidChange(session: WCSession)
{
if session.reachable {
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.text = "Reachable"
})
}
else
{
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.text = "Not Reachable"
})
}
}
}
}
in WatchExtention Code is
func someFunc() {
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
if session.reachable {
ispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Reachable")
})
}
else
{
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Not Reachable")
})
}
func sessionReachabilityDidChange(session: WCSession)
{
if session.reachable {
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Reachable")
})
}
else
{
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Not Reachable")
})
}
}
}
}
Now when enter to background in apple Watch the iPhone app showing Not reachable why ?
It's the default behavior of the AppleWatch, mainly to spare with resources like battery.
session.reachable property is true only when Apple Watch is reachable via Bluetooth and the associated Watch app is running in the
foreground in all other cases, the value is false.
In your case the second option which caused the problem I suppose the bluetooth connection is working.
Anyway the question is what do you like to reach.
Actually the simple rule is that you couldn't wake up the Watch from the iPhone but you could wake up the iPhone app from the Watch.
Two ways with three options to reach the watch when it's counterpart is in the background: send a complication update or send a message (2 options) in the background which will be available for the Watch when it will awake again.
All of them are part of the WCSession Class.
The two options for sending messages are:
- updateApplicationContext:error:
You can use this method to transfer a dictionary of data to the counterpart Watch app.iPhone sends context data when the opportunity arises, means when the Watch app arises.The counterpart’s session on the Watch gets the data with the session:didReceiveUpdate: method or from the receivedApplicationContext property.
You may call this method when the watch is not currently reachable.
The other option is sending data in the background like
- transferUserInfo:
You can use this method when you want to send a dictionary of data to the Watch and ensure that it is delivered. Dictionaries sent using this method are queued on the other device and delivered in the order in which they were sent. After a transfer begins, the transfer operation continues even if the app is suspended.
BUT true for both methods that they can only be called while the session is active. Calling any of these methods for an inactive or deactivated session is a programmer error.
The complication solution is a little bit different but belongs to the same WCSession Class as the earliers.
-transferCurrentComplicationUserInfo:
This method is specifically designed for transferring complication user info to the watch with the aim to be shown on the watch face immediately.
Of course it's available only for iOS, and using of this method counts against your complication’s time budget, so it's availability is limited.
The complication user info is placed at the front of the queue, so the watch wakes up the extension in the background to receive the info, and then the transfer happens immediately.
All messages received by your watch app are delivered to the session delegate serially on a background thread, so you have to switch to the main queue in case you'd like to use or presenting them for UI.
The WWDC talk on WatchConnectivity discusses "reachability" and its nuances in quite a lot of detail, so you should definitely give it a watch.
TL;DR: reachable on the watch is for the most part only going to be true/YES when the watch app's UI is visible on the screen.

Resources