VoIP Push Notifications Not Delivered in Background/Killed State (Using AppDelegate)

Hello, colleagues.

I am reaching out to the community with an issue that is likely quite common. Unfortunately, I have not been able to resolve it independently and would like to confirm if my approach is correct.

Core Problem: VoIP push notifications are not delivered to the application when it is in the background or terminated. After reviewing existing discussions on the forum, I concluded that the cause might be related to CallKit not having enough time to register. However, in my case, I am using AppDelegate, and in theory, CallKit registration should occur quickly enough. Nevertheless, the issue persists.

Additional Question: I would also like to clarify the possibility of customizing the CallKit interface. Specifically, I am interested in adding custom buttons (for example, to open a door). Please advise if such functionality is supported by CallKit itself, or if a different approach is required for its implementation.

Thank you in advance for your time and attention to my questions. For a more detailed analysis, I have attached a fragment of my code below. I hope this will help clarify the situation.

Main File

@main

struct smartappApp: App {
    @UIApplicationDelegateAdaptor private var delegate: AppDelegate
    @StateObject private var navigationManager = NavigationManager()
    init() {
        print("Xct")
        if !isRunningTests() {
            DILocator.instance
                .registerModules([
                    ConfigModule(),
                    SipModule(),
                    AuthModule(),
                    NetworkModule(),
                    CoreDataModule(),
                    RepositoryModule(),
                    DataUseCaseModule(),
                    SipUseCaseModule()
                ])
        }
    }
    func application(_ application: UIApplication,
                     didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                     fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        print("Axtest")
        completionHandler(.newData)
    }
    var body: some Scene {

App Delegate File

class AppDelegate: UIResponder, UIApplicationDelegate {
    
    private let voipRegistry = PKPushRegistry(queue: .main)
    private var provider: CXProvider?
    
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
       
        setupPushKit()
        
        setupCallKit()
        

        return true
    }
    public func regist(){
        setupPushKit()
        
        setupCallKit()
        
    }
    
    private func setupPushKit() {
        voipRegistry.delegate = self
        voipRegistry.desiredPushTypes = [.voIP]
        
        DispatchQueue.main.async {
            self.voipRegistry.pushToken(for: .voIP)
        }
    }
    
    // MARK: - CallKit Setup
    private func setupCallKit() {
        let configuration = CXProviderConfiguration(localizedName: "MyApp")
        configuration.maximumCallGroups = 1
        configuration.maximumCallsPerCallGroup = 1
        configuration.supportsVideo = true
        configuration.iconTemplateImageData = UIImage(named: "callIcon")?.pngData()
        
        provider = CXProvider(configuration: configuration)
        provider?.setDelegate(self, queue: .main)
        
    }
    
    
    // MARK: - Background Launch Handling
    func application(_ application: UIApplication,
                     didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                     fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        print("Axtest")
        completionHandler(.newData)
    }
}

extension AppDelegate: PKPushRegistryDelegate {
    
    func pushRegistry(_ registry: PKPushRegistry,
                      didUpdate pushCredentials: PKPushCredentials,
                      for type: PKPushType) {
        APNsImpl().registerToken(token: pushCredentials.token.map { String(format: "%02.2hhx", $0) }.joined())
    }
    
    func pushRegistry(_ registry: PKPushRegistry,
                      didReceiveIncomingPushWith payload: PKPushPayload,
                      for type: PKPushType) {

        
        guard type == .voIP else {
            return
        }
        print("call kit")
        let payloadDict = payload.dictionaryPayload

        
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .phoneNumber, value: "dsfsdfddsf")
        update.hasVideo = payloadDict["hasVideo"] as? Bool ?? false
        
        provider?.reportNewIncomingCall(with: UUID(),
                                       update: update,
                                       completion: { error in
            if let error = error {
                print("Failed to report incoming call: \(error.localizedDescription)")
            } else {
                print("Successfully reported incoming call")
            }
            
 
        })
    }
  
}


Core Problem: VoIP push notifications are not delivered to the application when it is in the background or terminated. After reviewing existing discussions on the forum, I concluded that the cause might be related to CallKit not having enough time to register.

Yes, that's sort of correct.

I think root of the problem is actually here:

    private let voipRegistry = PKPushRegistry(queue: .main)
    private var provider: CXProvider?

...and is tied to the dynamic of how static object initialization occurs. Basically, your PKPushRegistry object is going to be created IMMEDIATELY after your AppDelegate object is created and before basically any normal app methods are called, including didFinishLaunchingWithOptions.

I'm not sure how the full process plays out from there, but I think the net result is that you're "losing" the initial push because of the time gap between PKPushRegistry's creation and when didFinishLaunchingWithOptions is called and the rest of your initialization completes.

One thing to note here is that you can't really "miss" the push notification because your PKPushRegistry object was created to late. You actually have ~7s from the time callservicesd launches your app and when it would kill you for failing to report a new call. That's an ETERNITY, given that a standard app can complete the normal launch process in less than a second.

Similarly, the message itself can't actually be "lost"- the message has been queued for delivery and will be delivered to your app when your PKPushRegistry objects connects to callservicesd. Case in point, in the situation above I don't think the message was actually "lost"- the problem was that it reached your app and was then dropped because PKPushRegistry wasn't fully configured.

SO, here is how I would fix it:

  • As a general rule, I think statically initializing system objects like this is often a mistake, particularly at startup, since it means you no longer know or control the order things occur in.

  • To fix that, move your PKPushRegistry() initialization into didFinishLaunchingWithOptions, just like CXProvider.

  • It doesn't specifically matter here, but I would also create PKPushRegistry() after CXProvider, NOT before. You want everything "ready" to report a call, so CXProvider should exist first.

As a side note, I would change this:

provider?.reportNewIncomingCall(with: UUID(),
						   update: update,
						   completion: { error in

to this:

provider!.reportNewIncomingCall(with: UUID(),
						   update: update,
						   completion: { error in

You're going to crash if you fail to report a call, so it's actually better to crash when you force unwrap a NULL provider than it is to crash later when the system kills you. Crashing at the force unwrap tells you why you crashed, while crashing later leaves things a mystery.

Additional Question: I would also like to clarify the possibility of customizing the CallKit interface. Specifically, I am interested in adding custom buttons (for example, to open a door). Please advise if such functionality is supported by CallKit itself,

No, this is not possible. The CallKit screen you see is system UI that's totally outside of your apps control.

or if a different approach is required for its implementation.

I don't know what your larger goal is, but there simply isn't ANY way to customize the background call UI, particularly without losing the larger benefits CallKit provides (like not having other audio break calls).

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

VoIP Push Notifications Not Delivered in Background/Killed State (Using AppDelegate)
 
 
Q