Repository navigation
feat: report a refused startCall with startCallFailed (iOS, Android) - #911
Merged
Merged
Conversation
When iOS or Android refused a call started with startCall, JavaScript was never told: didReceiveStartCallAction didn't come, and the app kept a call the system didn't show. iOS: requestTransaction sends startCallFailed when CallKit refuses a CXStartCallAction, with CallKit's error and errorCode. The completion runs on CallKit's private queue, so the event is sent from the main queue, where the events are buffered, and getInitialEvents and didLoadWithEvents hand React Native a copy of the buffer. Android: VoiceConnectionService overrides onCreateOutgoingConnectionFailed. Telecom can refuse a call before giving it startCall's extras, so the calls startCall placed are remembered until Telecom creates or refuses them, and found by number when the request has no UUID. Based on #878 by @mjerris.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When the system refuses a call started with
startCall, JavaScript is never told:didReceiveStartCallActiondoesn't come, the system shows no call, and the app keeps whatever it started for it (a SIP INVITE, a WebRTC leg). This adds astartCallFailedevent on iOS and Android:It supersedes #878 (@mjerris), which reported refused CallKit transactions on iOS only (
didFailCallAction), and takes in its review.iOS
requestTransaction:sendsstartCallFailedwhen CallKit refuses aCXStartCallAction.errorCodeis mapped fromCXErrorDomainRequestTransaction, named likedidDisplayIncomingCall'serrorCode; code 8 (CallIsProtected) is matched as a literal for older SDKs, as in feat(ios): emitdidFailCallActionwhen CallKit refuses a requested action #878.startCall: a refusedendCall(UnknownCallUUID) loops with a handler that cleans up withendCall(feat(ios): emitdidFailCallActionwhen CallKit refuses a requested action #878 review).com.apple.callkit.callcontroller.completion). The event is sent from the main queue, where the provider's delegate buffers events, andgetInitialEventsanddidLoadWithEventshand React Native a copy of the buffer, read on the main queue (clearInitialEventsruns there too). React Native converts the array on the JS thread: converting the live array while it grew crashed withCollection was mutated while being enumeratedin the feat(ios): emitdidFailCallActionwhen CallKit refuses a requested action #878 review.NotificationCenter, now on the main thread.Android
VoiceConnectionServiceoverridesonCreateOutgoingConnectionFailed, which CallKeep never did: nothing reached JavaScript.startCall's extras: on Android 10 and 16, the request for a second managed call while the first is dialing has only the number (Bundle[]). SostartCallremembers the calls it placed until Telecom creates or refuses them, and the refused one is found by number (the latest with it). When the request carries the UUID (seen on Android 16 for a refusal at connection creation), that is used.startCalldidn't place (from the Phone app) sends nothing: JavaScript doesn't know it.hasOutgoingCallisn't reset: in the tested case, the first call is still dialing.ConnectionService(cancelPendingCall), so no event is possible. The extras remembered for such a call stay in the list (oneBundleper cancelled call).Docs, types, example
actions.js,index.d.ts,docs/events.md(table, section,NotificationCenterlist), a line understartCallindocs/api.md, the changelog, and a handler in the example app. The docs warn thatCallUUIDAlreadyExistsmeans the call with this UUID exists and goes on, so the app must not end it (#878 review).Testing
Android:
tests/android/CallKeepStartCallFailedTest.kt, a new instrumentation fixture with the real Telecom service in managed mode. The first call is dialing; a second call to+33 6 12 34 56 78getsstartCallFailedwith its own UUID, name and number; the first call goes on and gets no event.startCallFailedstartCallFailedThe self-managed confirmation was seen on both emulators (Telecom logs
Prompting user to disconnect self-managed call, thencancelPendingCallon Cancel, and noonCreateOutgoingConnectionFailed).iOS: iPhone 15 Pro Max, iOS 27.0.1, real CallKit, this branch's
RNCallKeep.mwith minimal React stubs (the harness from the #878 review):startCallX twice: the second getsstartCallFailedwithCallUUIDAlreadyExists, and X stays live inCXCallObserver. The event and the notification come on the main thread, withcallUUID(lowercase),handle,name,erroranderrorCode.maximumCallGroups: '1', X live,startCallY:MaximumCallGroupsReachedfor Y; X goes on.startCalls buffered while JS loops ongetInitialEventsand converts the result on a JS queue: this branch without the main-queue hop and the copies, 1,241 and 1,246mutated while being enumerated; this branch, 0 and 0, with all 3,000 events buffered.startCall(no other call) was refused by CallKit withMaximumCallGroupsReached, in 2 first launches out of 2, and in none of 2 later launches. master does the same silently; the app now getsstartCallFailed.didLoadWithEventswith buffered failures (same copy, read on the main queue).-Wall -Wextra, arm64-apple-ios15.1).Checks:
npm run checkpasses (18 events consistent). The example app type-checks, and a misspellederrorCodeis rejected.