feat: keep the iOS system call screen from taking over on simultaneous incoming calls (WT-1678) - #354
Draft
SERDUN wants to merge 12 commits into
Draft
feat: keep the iOS system call screen from taking over on simultaneous incoming calls (WT-1678)#354SERDUN wants to merge 12 commits into
SERDUN wants to merge 12 commits into
Conversation
… answered Answering one of two ringing incoming calls used to leave the other one registered, so iOS covered the app with its full-screen call-waiting prompt. Now the plugin takes the remaining ringing call out of the system for that moment: it keeps ringing inside the app and re-enters the system flow the instant it is answered, from whichever UI the answer came. Late pushes and catching-up signaling can no longer resurrect it. Verified by compiling the app against the branch.
The deferred-path logs and the exact transaction error now print in any build mode, so device sessions launched without Flutter tooling still show which stage failed. Temporary instrumentation.
The report completion may arrive off the main thread; mutating the plugin's call bookkeeping there raced the observer callback and could crash the app right before the answer was requested.
…erred calls A deferred call now re-enters CallKit under a brand-new identifier: the system happily accepts a re-report on the old one but hands back a call that often cannot be answered, leaving its screen stuck. A boundary translation keeps the Flutter side on the stable identifier. Deferral also now triggers on arrival - an incoming call while any call already lives in CallKit rings only inside the app, so the system screen no longer appears when a second call comes during a conversation.
The bookkeeping set can retain an ended call the observer never showed as ended, which made every following incoming call defer forever - the first call then rang only inside the app instead of through the system.
Re-reporting it as incoming raised the system incoming screen over the app and iOS kept the user on its own in-call screen after the answer. Started as outgoing, the call goes active silently and the user stays in the app; the start action is fulfilled inside the plugin and reaches Flutter as a plain answer of the original call. System call log shows such calls as outgoing - accepted trade-off.
When the push and signaling paths race to report the same incoming call, the losing path could mistake the call's own registry entry for a competing live call and mark it deferred. Declining then left a ghost ringing entry in CallKit, and answering brought up the system call-waiting screen instead of staying in the app. A call already live in CallKit is now always handled by the normal CallKit actions, and a stale deferred mark is cleared before any answer or end decision trusts it. Verified against the exact failing sequence captured on a device.
The call observer reports an ended call as still live for a moment after the plugin ends it. In that window the deferred-registration guards could mistake the fading entry for a real registration: a freshly deferred call lost its deferred mark, its answer then targeted a dead CallKit entry, and hold/end requests failed with unknown-call errors. Calls the plugin has reported ended are now remembered until the observer confirms the end, and every liveness decision skips them.
The mechanism is now opt-in via an option flag and off by default, so apps keep the standard iOS call-waiting behavior until they enable it. A deferred call also stops being a second-class one: the in-call call-waiting tone announces it during a conversation, an unanswered one leaves the usual missed-call record and notification, and when the visible call ends while a deferred one still rings, it surfaces back to the lock screen and ringtone instead of dying silently.
Review follow-up for the deferred CallKit registration mechanism: - A failed answer no longer strands calls: the re-attach rolls its alias state back when CallKit refuses the transaction, and calls deferred for an answer that then fails are brought back into the system UI. - Registration paths dedup against the promoted alias of a call instead of registering a duplicate ringing entry. - The defer pass skips entries already being ended, ended-report bookkeeping is pruned once the registry forgets the entry, and tearDown clears all mechanism state. - The missed-call journal entry waits until no call is live (no prompt flash next to an active call), is written only for calls without an earlier journal record, and its throwaway entry is never answerable. - Caller-name updates now reach the cached metadata used for re-entry and journaling.
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.
Overview
When two incoming calls ring at the same time on iOS and the user answers one of them, the system takes over with a full-screen call-waiting UI for the other call, leaving the app's own call screen in a wrong or incomplete state. In some flows the second call was not visible or audible at all.
The iOS plugin now keeps at most one call registered with the system while another one is being answered: the extra ringing call is held back and re-entered afterwards, so the system screen never appears and both calls stay manageable from the app. Verified on a real device with repeated dual-incoming-call cycles (no system screen, no stuck calls).
Known trade-off: the re-entered call is recorded in the system call log as outgoing. Draft until productization is done (diagnostics cleanup, opt-in flag, native tests).