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

You may not understand my question very well. My question is that ending/canceling a message does not match the method of adjusting the system's answer interface, which can cause a crash. I have tried removing the task directly, but it will crash on my end. It seems that Pushkit's method must be matched with the method of adjusting the system's answer interface?

how can me change livekit speaker's status,from in app to change

when i receive end/cancle voip notif,it

Voip pushes should ONLY be used for call notifications and nothing else. If you choose to use them for any other purpose (like call cancelation) then you risk triggering multiple incoming all reports, since EVERY call to "didReceiveIncomingPushWith" MUST also contain a call to "reportNewIncomingConversation".

Note that because the call reporting system allows you to skip reporting a call when a call is already active, it's very easy to create a solution that appear to work under "basic" testing but will fail badly under real world conditions.

I follow the method as discussing in this thread. and get through the problem which was boring me for some days. I think you can try just await the "reportNewIncomingConversation" like:

try await self.mgr.reportNewIncomingConversation(uuid: self.currentCallId, update: update) instead of run it in a Task.

No, this does NOT work. The issue here is that "try await" can only be called inside an async function. In concrete terms, there are two declarations of "didReceiveIncomingPushWith":

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

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

...and using "try await" requires using #2. However, the problem with #2 is that, architecturally, an async function like this IMMEDIATELY returns and does not block, which causes the current PushKit check to crash your app. Using #1 avoids that issue, but it also means that you cannot use "try await", which is why I used task.

I do in this way and it woks well now.

I would strongly recommend that you review your code and testing process again. More specifically, I would recommend building a dedicated test app, replicating the crashing case in that test app and then validating your code in the context of that test app. The threading and testing issue involved here are complex enough that it's VERY possible for code to "accidently" work, only to fail later.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

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