LiveCommunicationKit: report Incoming but Assertion failure in -[PKPushRegistry _terminateAppIfThereAreUnhandledVoIPPushes]

I completed the CallKit Demo with the same code.

When I changed to LiveCommunicationKit, the code goes perfectly when the app is in foreground, but it crashed in background. If I changed the reportIncoming method from LCK to CallKit, it goes well. What is the reason?

I changed the method from

func pushRegistry(_ registry: PKPushRegistry,
                      didReceiveIncomingPushWith payload: PKPushPayload,
                      for type: PKPushType, completion: @escaping () -> Void)

to

func pushRegistry(_ registry: PKPushRegistry,
                      didReceiveIncomingPushWith payload: PKPushPayload,
                      for type: PKPushType) async

it crashed before show the print "receive voip noti".

Here is the core code:

var providerDelegate: ProviderDelegate?

func pushRegistry(_ registry: PKPushRegistry,
                      didReceiveIncomingPushWith payload: PKPushPayload,
                      for type: PKPushType, completion: @escaping () -> Void) {
        if type != .voIP { return }
        guard let uuidString = payload.dictionaryPayload["uuid"] as? String,
              let uuid = UUID(uuidString: uuidString),
              let handle = payload.dictionaryPayload["handle"] as? String,
              let hasVideo = payload.dictionaryPayload["hasVideo"] as? Bool,
              let callerID = payload.dictionaryPayload["callerID"] as? String else {
            return
        }
 
        print("receive voip noti: \(type):\(payload.dictionaryPayload)")

        if #available(iOS 17.4, *) {
           // This code is only goes perfectly when the App is in foreground
            var update = Conversation.Update(members: [Handle(type: .generic, value: callerID, displayName: callerID)])
            if hasVideo {
                update.capabilities = [.video, .playingTones]
            } else {
                update.capabilities = .playingTones
            }

            Task { @MainActor in
                do {
                    print("LCKit report start")
                    try await LCKitManager.shared.reportNewIncomingConversation(uuid: uuid, update: update)
                    print("LCKit report success")
                    completion()
                } catch {
                    print("LCKit report failed")
                    print(error)
                    completion()
                }
            }
        } else {
            // It went perfectly 
            providerDelegate?.reportIncomingCall(uuid: uuid, callerID: callerID, handle: handle, hasVideo: hasVideo) { _ in
            completion()
        }
    }

@available(iOS 17.4, *)
final class LCKitManager {
    static let shared = LCKitManager()
    let manager: ConversationManager

    init() {
        manager = ConversationManager(configuration: type(of: self).configuration)
        manager.delegate = self
    }

    static var configuration: ConversationManager.Configuration {
        ConversationManager.Configuration(ringtoneName: "Ringtone.aif",
                                          iconTemplateImageData: #imageLiteral(resourceName: "IconMask").pngData(),
                                          maximumConversationGroups: 1,
                                          maximumConversationsPerConversationGroup: 1,
                                          includesConversationInRecents: true,
                                          supportsVideo: false,
                                          supportedHandleTypes: [.generic])
    }
    
    func reportNewIncomingConversation(uuid: UUID, update: Conversation.Update) async throws {
        try await manager.reportNewIncomingConversation(uuid: uuid, update: update)
    }
}

final class ProviderDelegate: NSObject, ObservableObject {
    static let providerConfiguration: CXProviderConfiguration = {
        let providerConfiguration: CXProviderConfiguration
        if #available(iOS 14.0, *) {
            providerConfiguration = CXProviderConfiguration()
        } else {
            providerConfiguration = CXProviderConfiguration(localizedName: "Name")
        }
        providerConfiguration.supportsVideo = false
        providerConfiguration.maximumCallGroups = 1
        providerConfiguration.maximumCallsPerCallGroup = 1
        let iconMaskImage = #imageLiteral(resourceName: "IconMask")
        providerConfiguration.iconTemplateImageData = iconMaskImage.pngData()
        providerConfiguration.ringtoneSound = "Ringtone.aif"
        providerConfiguration.includesCallsInRecents = true
        providerConfiguration.supportedHandleTypes = [.generic]
        return providerConfiguration
    }()

    private let provider: CXProvider

    init( {
        provider = CXProvider(configuration: type(of: self).providerConfiguration)
        super.init()
        provider.setDelegate(self, queue: nil)
    }

    func reportCall(uuid: UUID, callerID: String, handle: String, hasVideo: Bool, completion: ((Error?) -> Void)? = nil) {
        let callerUUID = UUID()
        let update = CXCallUpdate()
        update.remoteHandle = CXHandle(type: .generic, value: callerID)
        update.hasVideo = hasVideo
        update.localizedCallerName = callerID
        // Report the incoming call to the system
        provider.reportNewIncomingCall(with: callerUUID, update: update) { [weak self] error in
            completion?(error)
        }
    }
}
Answered by DTS Engineer in 826593022

Expanding on what I said here:

The API contract fo PKPushRegistry specifically requires that you report a call before returning. Trying to bounce to the main thread like this means that you did NOT in fact do that.

The issue I was describing here wasn't about the specific thread, it was about separating the execution stream. In concrete term, using "Task" means "do this later" and you cannot do that here. Here is the code modified to remove Task:

    @objc func startConversation(targetHandleValue : String,uuid : String, completion: @escaping (Bool, Error?) -> Void ) async {
....
        let incomingCallerHandle = Handle(type: .generic, value: targetHandleValue ,displayName: targetHandleValue)
            do {
                try await conversationManager.reportNewIncomingConversation(uuid: conversationUUID, update: update)
                completion(true, nil)
            } catch {
                print("An error occurred: \(error.localizedDescription)")
                completion(false, error)
            }
        }

Covering a few other details:

In fact, the same processing logic works correctly in the background when using CallKit.

I'd be curious to see what your CallKit code looked like, but the "Task" usage above will crash CallKit in EXACTLY the same way LiveCommunicationKit is crashing.

When I switch to LiveCommunicationKit, It also displays normally when the application in the foreground, this issue only occurs in the background.

Yes, this is expected behavior in both CallKit and LiveCommunicationKit. App are NOT required to report calls if:

  1. They are in the foreground.

  2. They already have an active call.

Again, that behavior is exactly the same between CallKit and LiveCommunicationKit.

And the console only prints "Received push" without "LCKit Start Answer"

Yes, because that's what your code actually "does". Note that if you modified your code to print like this:

...
   print("Received push")

    // !!!!!!! 
    // Whatever I write `@MainActor in` or remove this code,  Most of the time it's wrong when the app is in background
    Task {
        print("LCKit Start Answer")
....
    }
	print("After Task")
....

What will print in the foreground is:

Received push
After Task
LCKit Start Answer

And what will happen in the background is:

Received push
After Task
<App Crash>

...because you returned from didRecieveIncomingPush BEFORE you called reportNewIncomingConversation.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I init the pushRegistry with main queue and use the method "func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) async", it crash immediately before the print "receive voip noti".

if I init the pushRegistry with a custom queue, it will show the print "receive voip noti", and crashed after the print "LCKit report start"

let pushRegistry = PKPushRegistry(queue: DispatchQueue(label: "LCKit", qos: .userInteractive))

Assertion Stack:


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.' *** First throw call stack: (0x1a6774f20 0x19e5f32b8 0x1a5c7b7f8 0x2191b09f8 0x1ae61add4 0x1ae62a2c4 0x2191afc60 0x1ae61913c 0x1ae61add4 0x1ae6295a4 0x1ae6291b8 0x1a6747710 0x1a6744914 0x1a6743cd8 0x1eb1951a8 0x1a8d7dae8 0x1a8e31d98 0x1043927b8 0x1c9f1b154) libc++abi: terminating due to uncaught exception of type NSException


Assertion failure in -[PKPushRegistry _terminateAppIfThereAreUnhandledVoIPPushes], PKPushRegistry.m:349

When I changed to LiveCommunicationKit, the code goes perfectly when the app is in foreground, but it crashed in background. If I changed the reportIncoming method from LCK to CallKit, it goes well. What is the reason?

The issue is your attempt to transfer to the main thread here:

   Task { @MainActor in
                do {
                    print("LCKit report start")
                    try await LCKitManager.shared.reportNewIncomingConversation(uuid: uuid, update: update)
                    print("LCKit report success")
                    completion()
                } catch {
                    print("LCKit report failed")
                    print(error)
                    completion()
                }
    }

The API contract fo PKPushRegistry specifically requires that you report a call before returning. Trying to bounce to the main thread like this means that you did NOT in fact do that. You scheduled your call report call code to run on the main thread, then returned... at which point you crashed.

You can solve this in one of two ways:

  1. Set your PKPushRegistry to target the main queue and do everything there.

  2. Use the same queue for LiveCommunicationKit and call reportNewIncomingConversation directly instead of trying to change threads.

Of the two, I have a mild preferences for #1. In practice, properly written CallKit and PushKit delegates both block for minimal amounts of time and, if they are blocking for longer periods, that will cause other problems* which you'll need to fix. That means that there's very little reason not just use the main thread for both.

*I could try to be more specific, but I've never actually seen a developer fail this way.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

I'm facing the same problem. Could you please tell me how you solved it? oc

PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
pushRegistry.delegate = self;
pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];

  • (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void(^)(void))completion{ NSLog(@"Received void push notification data"); if (type == PKPushTypeVoIP) { NSDictionary *callKit = [payload.dictionaryPayload objectForKey:@"callkit"]; NSString *handle = [callKit objectForKey:@"handle"]; NSString *uuidString = [callKit objectForKey:@"uuid"]; self.callInfo = payload.dictionaryPayload; // 发起对话 if (@available(iOS 17.4, *)) { [[LiveCommunicationWrapper shared] startConversationWithTargetHandleValue:handle uuid:uuidString completion:^(BOOL status, NSError * _Nullable error) { completion(); }]; } else { completion(); } } else { NSLog(@"Received push notification of type: %@", type); completion(); }

}

swift

@available(iOS 17.4, *) @objc class LiveCommunicationWrapper: NSObject, ConversationManagerDelegate { @objc static let shared = LiveCommunicationWrapper()

let conversationManager = ConversationManager(configuration: ConversationManager.Configuration(ringtoneName: "default_ringtone", iconTemplateImageData: nil, maximumConversationGroups: 1, maximumConversationsPerConversationGroup: 1, includesConversationInRecents: false, supportsVideo: false, supportedHandleTypes: [.generic]))

private  override init() {
    super.init()
    conversationManager.delegate = self
    print("conversationManager  init.")
}

@objc func startConversation(targetHandleValue : String,uuid : String, completion: @escaping (Bool, Error?) -> Void ){
    guard let conversationUUID = UUID(uuidString: uuid) else {
        let error = NSError(domain: "InvalidUUID", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid UUID string"])
        completion(false, error)
        return
    }
    let hasVideo = false;
    var update = Conversation.Update(members: [Handle(type: .generic, value: uuid, displayName: uuid)])
    if hasVideo {
        update.capabilities = [.video, .playingTones]
    } else {
        update.capabilities = .playingTones
    }
    let incomingCallerHandle = Handle(type: .generic, value: targetHandleValue ,displayName: targetHandleValue)
    
    Task {
        do {
            try await conversationManager.reportNewIncomingConversation(uuid: conversationUUID, update: update)
            completion(true, nil)
        } catch {
            print("An error occurred: \(error.localizedDescription)")
            completion(false, error)
        }
    }
}

In fact, the same processing logic works correctly in the background when using CallKit.

When I switch to LiveCommunicationKit, It also displays normally when the application in the foreground, this issue only occurs in the background.

And the console only prints "Received push" without "LCKit Start Answer"


let pushRegistry = PKPushRegistry(queue: .main)

if #available(iOS 17.4, *) {
    var update = Conversation.Update(members: [Handle(type: .generic, value: callerName, displayName: callerName)])
    update.capabilities = .playingTones

    print("Received push")

    // !!!!!!! 
    // Whatever I write `@MainActor in` or remove this code,  Most of the time it's wrong when the app is in background
    Task {
        print("LCKit Start Answer")
        do {
            try await LCKitManager.shared.reportNewIncomingConversation(uuid: uuid, update: update)
            print("LCKit Answer Success")
            completion()
        } catch {
            print(error)
            completion()
        }
    }
}

Console output:

Received push
*** 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.'
*** First throw call stack:
(0x1a6774f20 0x19e5f32b8 0x1a5c7b7f8 0x2191b09f8 0x1ae61add4 0x1ae62a2c4 0x2191afc60 0x1ae61913c 0x1ae61add4 0x1ae6295a4 0x1ae6291b8 0x1a6747710 0x1a6744914 0x1a6743cd8 0x1eb1951a8 0x1a8d7dae8 0x1a8e31d98 0x104faeb84 0x1c9f1b154)
libc++abi: terminating due to uncaught exception of type NSException
*** Assertion failure in -[PKPushRegistry _terminateAppIfThereAreUnhandledVoIPPushes], PKPushRegistry.m:349

After testing many times, the success rate of displaying the pop-up when the app is killed is relatively normal, with only occasional failures. However, the failure rate is very high when the app is in the background.

Moreover, when the app receives a VoIP notification and reports it while in the foreground, and then transitions to the background, the success rate of reporting the VoIP notification in the background is significantly increased.

Expanding on what I said here:

The API contract fo PKPushRegistry specifically requires that you report a call before returning. Trying to bounce to the main thread like this means that you did NOT in fact do that.

The issue I was describing here wasn't about the specific thread, it was about separating the execution stream. In concrete term, using "Task" means "do this later" and you cannot do that here. Here is the code modified to remove Task:

    @objc func startConversation(targetHandleValue : String,uuid : String, completion: @escaping (Bool, Error?) -> Void ) async {
....
        let incomingCallerHandle = Handle(type: .generic, value: targetHandleValue ,displayName: targetHandleValue)
            do {
                try await conversationManager.reportNewIncomingConversation(uuid: conversationUUID, update: update)
                completion(true, nil)
            } catch {
                print("An error occurred: \(error.localizedDescription)")
                completion(false, error)
            }
        }

Covering a few other details:

In fact, the same processing logic works correctly in the background when using CallKit.

I'd be curious to see what your CallKit code looked like, but the "Task" usage above will crash CallKit in EXACTLY the same way LiveCommunicationKit is crashing.

When I switch to LiveCommunicationKit, It also displays normally when the application in the foreground, this issue only occurs in the background.

Yes, this is expected behavior in both CallKit and LiveCommunicationKit. App are NOT required to report calls if:

  1. They are in the foreground.

  2. They already have an active call.

Again, that behavior is exactly the same between CallKit and LiveCommunicationKit.

And the console only prints "Received push" without "LCKit Start Answer"

Yes, because that's what your code actually "does". Note that if you modified your code to print like this:

...
   print("Received push")

    // !!!!!!! 
    // Whatever I write `@MainActor in` or remove this code,  Most of the time it's wrong when the app is in background
    Task {
        print("LCKit Start Answer")
....
    }
	print("After Task")
....

What will print in the foreground is:

Received push
After Task
LCKit Start Answer

And what will happen in the background is:

Received push
After Task
<App Crash>

...because you returned from didRecieveIncomingPush BEFORE you called reportNewIncomingConversation.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

The CallKit code is a sample report without Task, it is running correctly

I used Task for LiveCommunicationKit because the ConversationManager.reportIncomingNewConversation method can only be called with await.

And the success rate is relatively high when the app is not running, but it is highly likely to fail when the app is in the background.

        if #available(iOS 17.4, *) {
            var update = Conversation.Update(members: [Handle(type: .generic, value: callerName, displayName: callerName)])
            update.capabilities = .playingTones
            print("Received push")
            Task {
                print("LCKit Start Answer")
                do {
                    try await LCKitManager.shared.reportNewIncomingConversation(uuid: uuid, update: update)
                    print("LCKit Answer Success")
                    completion()
                } catch {
                    print(error)
                    completion()
                }
            }
        } else {
            let update = CXCallUpdate()
            update.remoteHandle = CXHandle(type: .generic, value: callerName)
            update.localizedCallerName = callerName
            provider.reportNewIncomingCall(with: uuid, update: update) { _ in
                completion()
            }
        }

If the issue is related to Task, it still reports error in the background even after I changed the PushKit delegate method to func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) async, it still reports errors in the background. It even doesn't print received push when the pushRegistry is init with main queue, and it prints received push and LCKit Start Answer when the pushRegistry is init with a custom queue.

If the application is not running, the

    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) async {
        print("received push")
        if type != .voIP { return }

        let dic = payload.dictionaryPayload
        guard let uuidString = dic["uuid"] as? String,
              let uuid = UUID(uuidString: uuidString),
              let callerName = dic["callerName"] as? String else {
            return
        }

        if #available(iOS 17.4, *) {
            let update = Conversation.Update(members: [Handle(type: .generic, value: callerName, displayName: callerName)], capabilities: .playingTones)

            try? await LCKitManager.shared.reportNewIncomingConversation(uuid: uuid, update: update)
        }
    }

I used Task for LiveCommunicationKit because the ConversationManager.reportIncomingNewConversation method can only be called with await.

Yes? Why is it a problem to call it with "await"?

And the success rate is relatively high when the app is not running, but it is highly likely to fail when the app is in the background.

What do you mean by "success rate" here? And which implementation is failing in the the background case, "Task" or "await"?

It even doesn't print received push when the pushRegistry is init with main queue

I don't know why that would be, but there maybe some additional entanglements with how async handling interact with GCD. In any case, the specific threads involved are not the issue, Task is. Using a private queue for this is fine.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Yes? Why is it a problem to call it with "await"?

I don't know what the reason is. I suspect that the issue is caused by the asynchronous handling of "Task".

Because I compared the two reporting methods of CallKit and the reporting method of LiveCommunicationKit using code. When using the PushKit callback method func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void), only the block-based reporting method of CallKit successfully reports the VoIP. Both the async reporting methods of CallKit and LiveCommunicationKit encounter this issue in the background.

And I compared the two func receiveIncomingPush methods of PushKit, the async func receiveIncoming also encounters this issue in the background.

The only method that works successfully in all situations is calling the Completion block method of CallKit's reportIncoming.

What do you mean by "success rate" here? And which implementation is failing in the background case, "Task" or "await"?

"Success rate" means the voip report success rate in the background. The await implementation is failed in the background case, Task always run successfully.

I don't know why that would be, but there maybe some additional entanglements with how async handling interact with GCD. In any case, the specific threads involved are not the issue, Task is. Using a private queue for this is fine.

Yes, and I found a Voice SDK's README, they also recommend a private queue to init the PKPushRegistry to avoid the failure.

Accepted Answer

The only method that works successfully in all situations is calling the Completion block method of CallKit's reportIncoming.

Yeah, I see what's going on now.

First off, please file a bug on this issue and post the bug number back here. As things currently stand the PushKit async delegate is simply broken and cannot be used. This is an issue for both CallKit and LiveCommunicationKit, however, LiveCommunicationKit obviously makes the situation much worse because it doesn't have the legacy completion handler alternative CallKit provides.

In terms of workarounds, the simplest solution I've come up with is to use the completion handler (not async) delegate and to sleep the delegate thread before you return to ensure your task has entered reportNewIncomingConversation.

In code, that looks something like this:

func pushRegistry(_ registry: PKPushRegistry,
                      didReceiveIncomingPushWith payload: PKPushPayload,
                      for type: PKPushType, completion: @escaping () -> Void)
{
...
	Task {
		do {
			try await manager.reportNewIncomingConversation(uuid: uuid, update: update)                
		} catch {
			print("forceStart error: \(error)")
		}
	}
...
//do any additional work you want.	
...
	Thread.sleep(forTimeInterval: 0.05)
	completion()
}

The sleep there simply stalls the delegate thread long enough that reportNewIncomingConversation can be entered, at which point the crash will not occur.

Some notes on this approach:

  • Blocking the main thread for 0.05s should not be problematic, however, using your own queue for it is probably a good idea as an additional safety margin.

  • The sleep interval is somewhat arbitrary, but is basically the time necessary plus an additional (substantial) safety factor. For reference, I tested on a iPhone 12 mini (safe with 0.007+s) and an iPhone 14 Pro Max (safe with 0.005+s), so 0.05s is significantly more time than is absolutely required. However, I'd certainly encourage you to do your own broader testing and I'll also say that you could block for significantly longer than 0.05s without it being a serious issue for the PushKit.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Have you solved this crashing problem

The Feedback assistant number: FB16655952

Answering two at a time...

Have you solved this crashing problem

The workaround in my previous post fixes the crash.

The Feedback assistant number: FB16655952

Perfect, thank you for the bug and for your patience while we sort this out.

Just to let you know, it's very likely that your bug will be closed as a duplicate in the next few weeks/months but that does NOT mean you shouldn't have filed a bug. As is often the case, the engineering team was already aware of the issue and had an existing bug (in this case, because the team and I heavily discussed the issue yesterday). The goal of your bug was to tell the engineering team about the issue, it was to document the real world impact to help ensure proper prioritization.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

now i receive inviter voip notifi, use kevin apply method,it can work nice.but when i receive end/cancle voip notif,it will crash,is only invite notifi use - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void(^)(void))completion,other voip notifi can't?

LiveCommunicationKit: report Incoming but Assertion failure in -[PKPushRegistry _terminateAppIfThereAreUnhandledVoIPPushes]
 
 
Q