My questions are:
What exactly counts as “timely”?
You must call "reportNewIncomingCall" before you return from the PushKit delegate handler.
Does CallKit need to be reported within the same run loop, or within a specific time window (for example, within 1 second)?
Strictly speaking, what's actually happening is that PKPushRegistry is checking a flag that CallKit sets (basically "did report call") as soon as you return from the delegate handler. If that flag isn't set, then it throws an exception, crashing your app.
More experienced developers might realize that this is not in fact a secure enforcement mechanism since, with enough work, it seems like it would be possible for an app to disrupt that mechanism. That is in fact true and, in fact, with enough digging, you can actually find the mechanism CallKit uses to notify PushKit of the call. I won't describe the details, but it isn't particularly difficult to "spoof" that mechanism. That's because the role of this particular crash isn't to formally enforce the requirement, but is instead to make it as obvious as possible what the problem actually is.
The ACTUAL enforcement mechanism is a second check in callservicesd that happens a few seconds (~7s) after the push is delivered to your app. Failing that check will then cause callservicesd to kill your app with the exception code "0xbaadca11".
The VAST majority of apps fail at the first check, which is easy to identify through the exception backtrace (which will show "PKPushRegistry") and this reason screen:
"Killing app because it never posted an incoming call to the system after receiving a PushKit VoIP push."
However, mistakes like failing to configure your PKPushRegistry delegate at launch or blocking too long in your PKPushRegistry delegate can cause the second crash.
Does the completion handler also have a similar timing requirement?
No. The completion handler has no role in any of this and probably shouldn't be in the API at all (at least not for VoIP). It was added many years when PushKit's role was expanded to other specialty pushes, but it never really "did" anything useful for VoIP pushes. It has a minor resource management role (so you should call it), but no role at all with any of this.
- Cold launch delay
When the app is not running, a VoIP push will cause the system to launch the app before calling didReceiveIncomingPushWithPayload. If the app launch process is slow, there may be a long delay between app launch and the push callback. Could this delay potentially cause the system to treat the push as improperly handled, resulting in a VoIP push restriction?
No, not really. The callservicesd check I mentioned above doesn't start until after the system has launched your app and callservicesd launch timeout is long enough that any app that fails that timeout is going to have much larger issues. Finally, keep in mind that your app controls when it sets up your PKPushRegistry delegate. If you're setting it up in applicationDidFinishLaunching (where we recommend), then the general system watchdog has tighter timing requirements than CallKit.
- Ending CallKit quickly when the app is in the foreground
When the app is already in the foreground, our current flow is:
Receive VoIP push
Report the call to CallKit
After a very short delay (around 0.5 seconds), programmatically end the CallKit call
Present the custom in-app call UI
Could this behavior potentially be considered misuse of VoIP push or CallKit by the system?
So, the direct answer is "no". Particularly in the foreground, the system doesn't really "care" what your app does. In terms of the background, I'd have a minor concern around WHY you're doing this, as there is a very long-standing policy requirement that “VoIP" be used for "Calling Apps". That is, the "point" of the “VoIP" background category is to allow "people to call other people". Using it for any other roles is not allowed.
However, as long as the incoming call requests are "real", then I don't see any issue with what you're doing. Your app must report the call; it does not have to answer it.
One minor note on this point:
Present the custom in-app call UI
By design, CallKit doesn't present its own UI when your app is in the foreground specifically so you can present your own call UI.
Having said that, I do have two technical concerns:
First off, my major question would be why are you doing this? CallKit is an INCREDIBLE tool for VoIP apps, as it shifts their audio out of the normal audio priority system, solving a bunch of fairly weird and ugly edge cases. Frankly, saying "I'd like to make a VoIP app without CallKit" is the same as saying "I'd like to make a VoIP app that doesn't work very well". If you're making a VoIP app and don't think you need CallKit, then you're either doing something VERY strange or you haven't tested your app very well.
Secondly, in my experience, mixing CallKit and direct audio session management doesn't actually work that well. Direct session activation can prevent CallKit session activation from working properly, leading to weird edge case failures. My longstanding advice to all developers has been "use CallKit for all your audio" as mixing it with direct session control just doesn't really work all that well.
If your use case really is that different, then that's something I'd like to understand better, as I suspect you might be better off dropping PushKit entirely, relying on standard push instead. Keep in mind that standard high-priority alert pushes have the same delivery priority as VoIP pushes [1], so PushKit doesn't provide that much benefit without CallKit.
[1] Both types are handled the same by our pushes server, as you can't really do better than "deliver this right NOW".
- Recovery after VoIP push restriction
If the system determines that the app has violated VoIP push rules and VoIP pushes stop arriving, the only workaround we are currently aware of is:
The user uninstalls and reinstalls the app.
Are there any other ways to recover from this state while preserving user data?
No, not that the user really controls. I believe the mechanism also resets if/when the app is updated, but that isn't really something users can "do".
Also, if the user does nothing, will the system automatically remove the restriction after some time?
Yes, it should reset every ~24 hours. However, keep in mind that ALL of these issues should be treated as failures in your app you need to fix. There's no reason a properly implemented VoIP app should EVER crash for failing to report a call.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware