CallKit: CXCallEndedReason.remoteEnded when the call is ended with reason=answerTimeout

Hello, experts!

During a VOIP call, the following happens:

  • device 1 makes a call to device 2
  • device 2 deliberately does not receive the call
  • some time passes, timeout is triggered and the call is terminated with CXCallEndedReason = remoteEnded, as evidenced by a line in the logs of the incoming call:
`[info] reportCallWasEnded callId=[***-***-***], reason=[CXCallEndedReason(rawValue: 2)].`

What is the reason why CXEndCallAction may be called from CXProviderDelegate even though the call was not manually terminated by clicking on the “End Call” button

Answered by DTS Engineer in 807524022

What is the reason why CXEndCallAction may be called from CXProviderDelegate even though the call was not manually terminated by clicking on the “End Call” button

Basically, because that's how it works. The system only has a single "call has ended" path, which is CXEndCallAction. One thing to be aware of here is that CallKit wasn't actually "designed" as an independant framework. It was originally created to implement cellular calling, then expanded to support Facetime and voip apps. This architecture has both costs and benefits- on the positive side, the behavior between 3rd party voip apps and Phone.app has been very consistent and reliable, however, it also means that much of the overall behavior was defined by "how the framework already worked". All of this comes from the fact that the same code is handling "everything". Case in point:

It looks like on the outgoing side the finish reason is correct - CXCallEndedReasonUnanswered, but on the receiving side it is handled incorrectly - CXCallEndedReason = remoteEnded

I'm not sure of exactly why it returns this, but I suspect it's a detail left over from the original phone implementation, where the remote end point was what ended calls. However, it may also be how the system differentiates between "I chose to not answer by the call" (for example, by clicking the power button to silence the call) and "I didn't interact with the call at all".

Having said all that, what's the issue you're trying to solve here?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Important note, after a timeout on the outgoing side, the following is invoked

[CallKitClientImpl] [info] reportCallWasEnded callId=[***-***-***], reason=[CXCallEndedReason(rawValue: 3)]

which is consistent with this transcript:

    CXCallEndedReasonFailed = 1, // An error occurred while trying to service the call
    CXCallEndedReasonRemoteEnded = 2, // The remote party explicitly ended the call
    CXCallEndedReasonUnanswered = 3, // The call never started connecting and was never explicitly ended (e.g. outgoing/incoming call timeout)
    CXCallEndedReasonAnsweredElsewhere = 4, // The call was answered on another device
    CXCallEndedReasonDeclinedElsewhere = 5, // The call was declined on another device
} API_AVAILABLE(ios(10.0), macCatalyst(13.0), watchos(9.0))  API_UNAVAILABLE(macos, tvos);

It looks like on the outgoing side the finish reason is correct - CXCallEndedReasonUnanswered, but on the receiving side it is handled incorrectly - CXCallEndedReason = remoteEnded

What is the reason why CXEndCallAction may be called from CXProviderDelegate even though the call was not manually terminated by clicking on the “End Call” button

Basically, because that's how it works. The system only has a single "call has ended" path, which is CXEndCallAction. One thing to be aware of here is that CallKit wasn't actually "designed" as an independant framework. It was originally created to implement cellular calling, then expanded to support Facetime and voip apps. This architecture has both costs and benefits- on the positive side, the behavior between 3rd party voip apps and Phone.app has been very consistent and reliable, however, it also means that much of the overall behavior was defined by "how the framework already worked". All of this comes from the fact that the same code is handling "everything". Case in point:

It looks like on the outgoing side the finish reason is correct - CXCallEndedReasonUnanswered, but on the receiving side it is handled incorrectly - CXCallEndedReason = remoteEnded

I'm not sure of exactly why it returns this, but I suspect it's a detail left over from the original phone implementation, where the remote end point was what ended calls. However, it may also be how the system differentiates between "I chose to not answer by the call" (for example, by clicking the power button to silence the call) and "I didn't interact with the call at all".

Having said all that, what's the issue you're trying to solve here?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

CallKit: CXCallEndedReason.remoteEnded when the call is ended with reason=answerTimeout
 
 
Q