We're developing a video calling application and rely on APNS VoIP notifications. Due to our design it sometimes happens that the VoIP notification arrives to the device when the call has already ended or the recipient has declined it (missed call for example).
The problem with that approach is that iOS requires you to report all incoming VoIP notifications in some way - either that there's new incoming call or the current call has been updated.
Is there any way to ignore the unnecessary/redundant VoIP notification? The current approach I came up with is really nasty i.e. first I report new unknown incoming call and then immediately I report its end. This causes the native call UI to be shown for a brief moment.
private var provider: CXProvider?
private var uuid = UUID()
//...
func ignorePushNotification() {
self.provider?.reportNewIncomingCall(with: self.uuid, update: CXCallUpdate(),
completion: { error in
// ignore
})
self.provider?.reportCall(with: self.uuid, endedAt: nil, reason: reason)
}
Unfortunately, there isn't a better way to ignore a VoIP Push. But I suggest you to improve the code as follows.
func ignorePushNotification() {
provider?.reportNewIncomingCall(
with: self.uuid,
update: CXCallUpdate(),
completion: { error in
self.provider?.reportCall(with: self.uuid, endedAt: nil, reason: .failed)
})
}
Given the asynchronous nature of CallKit, if you don't do that, it could happen that the end of the call executes before the reportNewIncomingCall. It's probably very rare but it could happen.
Related
My app uses a lot of scheduled local notifications and on certain events I reschedule the notifications and want to clear some of the delivered notifications, not all of them.
Rough pseudo code:
// Clear pending notifications that haven't been delivered yet
notificationCenter.removeAllPendingNotificationRequests()
// Get the delivered notifications (async), filter out the ones that should be removed
// and remove them
notificationCenter.getDeliveredNotifications() { notifications in
let notificationsToRemove = notifications.filter { some boolean operation }
let identifiersToRemove = notificationsToRemove.map { $0.identifier }
notificationCenter.removeDeliveredNotificationsWithIdentifiers(identifiersToRemove)
}
// Schedule the next set of notifications
let nextBatchOfNotifications = notificationGenerator.generate()
for notification in nextBatchOfNotifications) {
notificationCenter.schedule(notification)
}
But when I do this the vast majority of the time results in all delivered notifications being cleared. And in very rare cases it results in only some of the delivered notifications I ask to be removed being removed (or maybe none).
At least in my case it transpires that the async nature of all the functions related to querying and scheduling notifications was the problem and the fact removing pending/delivered notifications and trying to schedule new ones meant that iOS would get in a bit of a mess and not do what I asked properly.
The solution appears to be to wait for the delivered notifications to be returned, remove them, wait a little more and then schedule the new ones. Since I've made this change I've not seen any issues so far!
Rough pseudo code updated with the waits
// Clear pending notifications that haven't been delivered yet
notificationCenter.removeAllPendingNotificationRequests()
// Get the delivered notifications (async), filter out the ones that should be removed
var identifiersToRemove
var semaphore
notificationCenter.getDeliveredNotifications() { notifications in
let notificationsToRemove = notifications.filter { some boolean operation }
identifiersToRemove = notificationsToRemove.map { $0.identifier }
semaphore.signal()
}
semaphore.wait()
notificationCenter.removeDeliveredNotificationsWithIdentifiers(identifiersToRemove)
Thread.sleep(0.1)
// Schedule the next set of notifications
let nextBatchOfNotifications = notificationGenerator.generate()
for notification in nextBatchOfNotifications) {
notificationCenter.schedule(notification)
}
I'm not sure if there's a better way of doing the 0.1s wait after requesting for the delivered notifications to be removed or not... there's no callback to let me know that it's been done so it's the best I could come up with for now!
(apologies for the pseudo code if it's difficult to follow, my code is somewhat legacy so is still in Objective-C and I didn't think that was particularly appropriate to share in this day and age!)
Issue: Different Behavior In 3 Different Contexts
Ok so Ok, in iOS it seems three different things can happen regarding Push Notifications:
When a Push Notification is received when the app is not in the foreground
something shows up in Notification Center
if the app is opened by tapping the notification, either AppDelegate.DidReceiveRemoteNotification(...) or AppDelegate.ReceivedRemoteNotification(...) is called, apparently depending on which one is implemented (??).
if the app is opened without tapping the notification, only AppDelegate.WillEnterForeground(...), is called, without any explicit mention of the notification, and nothing else happens to acknowledge that a notification was received.
When a Push Notification is received when the app is in the foreground it causes the UNUserNotificationCenterDelegate, if there is one, to execute UNUserNotificationCenterDelegate.WillPresentNotification(...).
Approach: Routing To One Method From All Contexts
So to cover all bases with Push I need to implement something in all three methods: AppDelegate.DidReceiveRemoteNotification(...) / AppDelegate.ReceivedRemoteNotification(...), AppDelegate.WillEnterForeground(...), and UNUserNotificationCenterDelegate .WillPresentNotification(...).
Here are some stubs to show my approach to all this.
First, I created a custom UNUserNotificationCenterDelegate, with a Shared static member:
public class IncomingNotificationHandler : UNUserNotificationCenterDelegate
{
public static IncomingNotificationHandler Shared = new IncomingNotificationHandler();
...
}
Second, inside that class I made a handler that I can route to in every case (again, this is just a stub for debugging purposes):
//sets all parameters to null by default, so it can be called from methods
//that don't know anything about notifications:
public void HandleNotificationsIfAny(UIApplication application = null,
NSDictionary userInfo = null,
Action<UIBackgroundFetchResult> completionHandler = null)
{
//checks if userInfo is null, and logs its conclusions about that:
if (userInfo == null)
{
//In the null case, we can get pending notifications from
//UNUserNotificationCenter:
UNNotification[] pendingNotifications = new UNNotification[] { };
UNUserNotificationCenter.Current.GetDeliveredNotifications(returnedValue => pendingNotifications = returnedValue);
//Then we log the number of pending notifications:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): delivered notification count: " + pendingNotifications.Length);
//And make note of where this was probably called from:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): may have been called from this.WillPresentNotification(...) OR AppDelegate.WillEnterForeground(...)");
return;
});
}
else
{
//In the non-null case, we log the userInfo
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): just got info: " + userInfo);
//And make note of where this was probably called from:
Debug.WriteLine("IncomingNotificationHandler: HandleNotificationsIfAny(...): may have been called from AppDelegate.DidReceiveRemoteNotification(...)");
}
}
Third, inside the same class, I implemented the single method that's required by UNUserNotificationCenterDelegate, and I routed to the handler from it:
public override void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
HandleNotificationsIfAny();
}
Fourth, and last, inside AppDelegate, I routed to the same handler from both relevant methods:
//I prefer using DidReceiveRemoteNotification because in my experience
//the other one is sometimes not reliable:
public override void DidReceiveRemoteNotification(UIApplication application,
NSDictionary userInfo,
Action<UIBackgroundFetchResult> completionHandler)
{
//Simply passing on all the parameters called in this method:
IncomingNotificationHandler.Shared.HandleNotificationsIfAny(application, userInfo, completionHandler);
}
//WillEnterForeground also calls the handler without any parameters
//because it doesn't automatically know anything about notifications:
public override void WillEnterForeground(UIApplication application)
{
IncomingNotificationHandler.Shared.HandleNotificationsIfAny();
}
With that, as it stands, I think I'm handling a notification event in the same way no matter how my app is alerted about it, and even when it's not alerted at all.
Does anyone know if I now have it covered, or if there's some other cases I need to handle?
For the first scenario: AppDelegate.ReceivedRemoteNotification
It reflects the objective c method: application:didReceiveRemoteNotification:, but this event has been deprecated since iOS 10: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623117-application?language=objc. So I think there's no need to handle this event.
For the second scenario: AppDelegate.DidReceiveRemoteNotification
You can still utilize it to handle notifications now if you haven't implemented UNUserNotificationCenter and please notice it is only valid after iOS 7+. Moreover, this event will be triggered when app is on the foreground and if your app is on the background, this event only fires when the user clicks the notification to open your application. And there's no way to access the notification's information if the user clicks the icon to open the app.
I don't think handling AppDelegate.WillEnterForeground is a good approach, as it will be called each time the app resumes from background to foreground even though there are no notifications.
For the scenario: UNUserNotificationCenterDelegate
You could only use this feature after iOS 10. Once you have implemented it on the device iOS 10+, DidReceiveRemoteNotification and ReceivedRemoteNotification will never be triggered. WillPresentNotification will be called when app is on the foreground. DidReceiveNotificationResponse will be fired when the app is on the background and user clicks notifications to open it.
As a conclusion, if you want to easily handle the notification AppDelegate.DidReceiveRemoteNotification is enough. If you want to consume the new features of UNUserNotificationCenter, AppDelegate.DidReceiveRemoteNotification and UNUserNotificationCenter should be both involved. The prior one for the iOS 7+ devices and the later one for iOS 10+ devices.
Update:
For iOS 10+, you could use UNUserNotificationCenter.Current.GetDeliveredNotifications to obtain the notifications that are still displayed in Notification Center. And if you only want to support iOS version 10 and later. I think UNUserNotificationCenter is enough, there's no need to implement AppDelegate.DidReceiveRemoteNotification(...) or AppDelegate.ReceivedRemoteNotification(...).
If the app is on background / killed state and the user clicks notification to
open the app, DidReceiveNotificationResponse will be called.
If the
user clicks icon to open your app and the app is killed you should
place your logic code in FinishedLaunching.
If the user clicks icon
to open your app and app is on background, you can handle
WillEnterForeground as you did before.
If the app is on foreground,
handle WillPresentNotification.
I'm developing CallKit application, I have a problem, Call Holding is failing to restart audio when "swapping" calls on the CallKit screen until user returns to in-app call screen. I can bypass this by updating:
supportsHolding = false
but I can I solve this problem, whatsapp for example can do this correctly!
p.s. I'm using webrtc to make a call!
thanks!
EDIT:
This is code of provider:
public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
guard let call = conductor!.callWithUUID(uuid: action.callUUID) else {
WebRtcConductor.debug("\(self.TAG) 🔴 failed to perform HeldAction: uuid: \(action.uuid), calluiid: \(action.callUUID)")
action.fail()
return
}
setIsHeld(call: call, isHeld: action.isOnHold)
action.fulfill()
}
the setIsHeld function simply do:
audioTrack.isEnabled = enabled
If I use "mute" button of callkit screen, all works fine, but if I have 2 active calls, when I swipe from webrtc call to normal call, CXSetHeldCallAction is called and audio track did disabled, If I swipe again to webrtc call, audio track is enabled but i do not hear nothing, if I return to main app screen, audio works fine again!
Actually, there is a limitation in the Google WebRTC Library which leads to the described problem when implementing a CallKit integration which supports swapping calls.
The WebRTC Issue 8126 is known for over a year now, but not yet integrated into the WebRTC master branch. However, you can find the necessary code changes to fix this problem in the original ticket.
However, as a workarround, you can trigger the system notification which is subscribed by WebRTC internally.
Post a AVAudioSessionInterruptionType.ended Notification in the "didActivate audioSession" method of the CallKit Provider:
var userInfo = Dictionary<AnyHashable, Any>()
let interrupttioEndedRaw = AVAudioSessionInterruptionType.ended.rawValue
userInfo[AVAudioSessionInterruptionTypeKey] = interrupttioEndedRaw
NotificationCenter.default.post(name: NSNotification.Name.AVAudioSessionInterruption, object: self, userInfo: userInfo)
PS: Stare the ticket to improve chances of a merge ;-)
Had the same issue. If I have 1 active call, then new calls is incoming, I tap hold&accept. New call works, but after using Swap in CallKit audio stops working.
Found that provider:performSetHeldCallAction: method from CXProviderDelegate protocol is the spot where you can actually deactivate/activate audio for Swap calls via CallKit native interface.
In my case I used the audioController.deactivateAudioSession() method for the call was putting in OnHold.
But found that the same method provider:performSetHeldCallAction: was fired for other call that is being put active (from OnHold state), when tap Swap button via CallKit.
So you just need to deactivate/activate audio respectively to call's state (either hold or not).
In common way it should look this way:
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
// Retrieve the Call instance corresponding to the action's call UUID
guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
action.fail()
return
}
// Update the Call'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()
}
P.S. It looks like you should have some class that responds for Audio session. It should implement kind of activate audio session / deactivate audio session.
I am using call kit framework for calling and please help me
how to remove call kit ui when incoming call occurs during app in foreground and call in process, i am getting call kit ui in background.
You can check for app state before reporting a new incoming call to CXProvider. If you do not wish to use system incoming call screen when your app is in foreground, then give the if statement not to report new incoming call screen if the app is in foreground.
Example:
let state = UIApplication.shared.applicationState
if state == .background {
// background
provider.reportNewIncomingCall(with: UUID(uuidString: call.callUUID)!, update: callUpdate) { error in /* */ }
}
I'm trying to use CTCallCenter in Swift, however it always displays error.
I suppose it may cause in how to use closure but actually I don't familiar about it.
Does anybody have idea to resolve this issue?
Here is my code
import CoreTelephony
class ViewController: UIViewController{
var callCenter:CTCallCenter = CTCallCenter()
override func viewDidLoad() {
callCenter.callEventHandler(call:CTCall) -> Void in{
//will get CTcall status here
}
}
}
There are three errors.
1, Braced block of statements is an unused closure
2, Expected expression
3, Consecutive statements on a line must be separated by ";".
I tried to change as it indicated but any ways are not correct.
Thanks in Advance!
I got this working using the following code:
import CoreTelephony
class SomeClass: UIViewController {
private var callCenter = CTCallCenter()
override func viewDidLoad() {
super.viewDidLoad()
callCenter.callEventHandler = { (call:CTCall!) in
switch call.callState {
case CTCallStateConnected:
println("CTCallStateConnected")
self.callConnected()
case CTCallStateDisconnected:
println("CTCallStateDisconnected")
self.callDisconnected()
default:
//Not concerned with CTCallStateDialing or CTCallStateIncoming
break
}
}
}
func callConnected(){
// Do something when call connects
}
func callDisconnected() {
// Do something when call disconnects
}
}
Hope it helps.
From the Apple documentation :
Responding to Cellular Call Events
Dispatched when a call changes state.
Declaration:
var callEventHandler: ((CTCall!) -> Void)!
Discussion:
This property’s block object is dispatched on the default priority global dispatch queue when a call changes state. To handle such call events, define a handler block in your application and assign it to this property. You must implement the handler block to support being invoked from any context.
If your application is active when a call event takes place, the system dispatches the event to your handler immediately. However, call events can also take place while your application is suspended. While it is suspended, your application does not receive call events. When your application resumes the active state, it receives a single call event for each call that changed state—no matter how many state changes the call experienced while your application was suspended. The single call event sent to your handler, upon your application returning to the active state, describes the call’s state at that time.
For example, suppose your application changes from the active to the suspended state while a call is in the connected state. Suppose also that while your application is suspended, the call disconnects. When your application returns to the active state, you get a cellular call event indicating that the call is disconnected.
Here is a more complex example. Suppose your application changes from the active to the suspended state after the user has initiated a call but before it connects (that is, your application suspends while the call is in the dialing state). Suppose further that, while your application is suspended, the call changes first to the connected state and then to the disconnected state. When your application returns to the active state, you get a single cellular call event indicating that the call is disconnected.
May be now you can understand how to declare that.