Skip to content

fix: allow self-managed calls to numbers Android classifies as emergency (WT-1730)#349

Merged
SERDUN merged 5 commits into
developfrom
fix/callkeep-emergency-number-collision-sip-scheme
Jul 16, 2026
Merged

fix: allow self-managed calls to numbers Android classifies as emergency (WT-1730)#349
SERDUN merged 5 commits into
developfrom
fix/callkeep-emergency-number-collision-sip-scheme

Conversation

@SERDUN

@SERDUN SERDUN commented Jul 16, 2026

Copy link
Copy Markdown
Member

Overview

TelecomManager.placeCall() blocks a self-managed PhoneAccount from ever placing a call to a number Android classifies as emergency, based purely on the tel: address matching the device/SIM-region emergency-number list - regardless of whether that number is actually reachable as an emergency service. This silently drops calls to legitimate internal PBX extensions that happen to collide with that list (e.g. an extension literally named 112 or 911).

Building the outgoing Uri as a sip: address instead of tel: sidesteps the check entirely, since Android's emergency-number matching only ever applies to tel: addresses. The real number keeps flowing unchanged via CallMetadata into onCreateOutgoingConnection, which never reads the Uri - so this only affects what Telecom itself sees for its own emergency classification, not what actually gets dialed or how the call is presented afterwards (PhoneConnection.updateData() sets the connection's visible address from CallMetadata.number directly, independent of this Uri).

Verified against a mature SIP client (Linphone) placing a real call to a colliding number and inspecting Android's own Telecom system_server logs (EmergencyCallHelper: ... not emergency) to confirm the mechanism, then reproduced and validated the same behavior for our own self-managed account on-device, both with Android's adb emergency-number-test-mode synthetic override and with a real call against a live PortaSwitch test environment.

Marked as draft pending the standard QA window before merge.

Test plan

  • On-device: ordinary number still places and connects normally
  • On-device (synthetic test-mode emergency number, adb shell cmd phone emergency-number-test-mode): call reaches onCreateOutgoingConnection and connects, no onCreateOutgoingConnectionFailed
  • On-device (real call against a live SIP test environment, number colliding with the emergency list): call rings, gets accepted, connects with audio end-to-end

SERDUN added 5 commits July 16, 2026 16:49
TelecomManager.placeCall() blocks a self-managed PhoneAccount from ever
placing a call to a number the OS classifies as emergency, based purely
on the tel: address matching the device/SIM-region emergency-number
list - regardless of whether the number is actually reachable as one.
This silently drops calls to legitimate internal PBX extensions that
happen to collide with that list (e.g. an extension literally named
112 or 911).

Building the outgoing Uri as a sip: address instead of tel: sidesteps
the check entirely, since it only matches tel: addresses. The real
number keeps flowing unchanged via CallMetadata into
onCreateOutgoingConnection, which never reads the Uri - so this only
affects what Telecom itself sees for its own emergency classification,
not what actually gets dialed.
…coy sip: host

Same RFC-reserved, never-resolving intent (RFC 9476 .internal instead
of RFC 2606 .invalid), just a more identifiable name for this address
in logs.
The sip: scheme change means Android's Telecom never classifies our
outgoing calls as emergency anymore, so the code path that used to
catch and report an emergency-number failure can never run:

- removed EmergencyNumberException and its throw/catch sites
- removed the now-single-case-only OutgoingFailureType.EMERGENCY_NUMBER
- removed the now-unused TelephonyUtils.isEmergencyNumber() helper

The shared PCallRequestErrorEnum.emergencyNumber /
CallkeepCallRequestError.emergencyNumber wire values are left in place
deliberately: the Kotlin side encodes this enum by explicit raw Int
values while the Dart side decodes by list index, so removing an entry
from the middle would require re-aligning every value after it by hand
on both sides to avoid a silent wire-format mismatch. Nothing on the
Android side sets this value anymore either way.
@SERDUN
SERDUN marked this pull request as ready for review July 16, 2026 14:46
@SERDUN
SERDUN merged commit d01ea8a into develop Jul 16, 2026
3 checks passed
@SERDUN
SERDUN deleted the fix/callkeep-emergency-number-collision-sip-scheme branch July 16, 2026 14:46
SERDUN added a commit that referenced this pull request Jul 16, 2026
* fix: allow self-managed calls to numbers Android classifies as emergency (WT-1730) (#349)

* fix: allow self-managed calls to numbers Android classifies as emergency

TelecomManager.placeCall() blocks a self-managed PhoneAccount from ever
placing a call to a number the OS classifies as emergency, based purely
on the tel: address matching the device/SIM-region emergency-number
list - regardless of whether the number is actually reachable as one.
This silently drops calls to legitimate internal PBX extensions that
happen to collide with that list (e.g. an extension literally named
112 or 911).

Building the outgoing Uri as a sip: address instead of tel: sidesteps
the check entirely, since it only matches tel: addresses. The real
number keeps flowing unchanged via CallMetadata into
onCreateOutgoingConnection, which never reads the Uri - so this only
affects what Telecom itself sees for its own emergency classification,
not what actually gets dialed.

* build: use portadialer.internal instead of webtrit.invalid for the decoy sip: host

Same RFC-reserved, never-resolving intent (RFC 9476 .internal instead
of RFC 2606 .invalid), just a more identifiable name for this address
in logs.

* build: remove dead emergency-number exception handling on Android

The sip: scheme change means Android's Telecom never classifies our
outgoing calls as emergency anymore, so the code path that used to
catch and report an emergency-number failure can never run:

- removed EmergencyNumberException and its throw/catch sites
- removed the now-single-case-only OutgoingFailureType.EMERGENCY_NUMBER
- removed the now-unused TelephonyUtils.isEmergencyNumber() helper

The shared PCallRequestErrorEnum.emergencyNumber /
CallkeepCallRequestError.emergencyNumber wire values are left in place
deliberately: the Kotlin side encodes this enum by explicit raw Int
values while the Dart side decodes by list index, so removing an entry
from the middle would require re-aligning every value after it by hand
on both sides to avoid a silent wire-format mismatch. Nothing on the
Android side sets this value anymore either way.

* build: centralize outgoing decoy sip: Uri construction in TelephonyUtils

* build: percent-encode the number in the outgoing sip: Uri

* build: bump version build iteration to 1.3.0+2
SERDUN added a commit that referenced this pull request Jul 16, 2026
* fix: allow self-managed calls to numbers Android classifies as emergency (WT-1730) (#349)

* fix: allow self-managed calls to numbers Android classifies as emergency

TelecomManager.placeCall() blocks a self-managed PhoneAccount from ever
placing a call to a number the OS classifies as emergency, based purely
on the tel: address matching the device/SIM-region emergency-number
list - regardless of whether the number is actually reachable as one.
This silently drops calls to legitimate internal PBX extensions that
happen to collide with that list (e.g. an extension literally named
112 or 911).

Building the outgoing Uri as a sip: address instead of tel: sidesteps
the check entirely, since it only matches tel: addresses. The real
number keeps flowing unchanged via CallMetadata into
onCreateOutgoingConnection, which never reads the Uri - so this only
affects what Telecom itself sees for its own emergency classification,
not what actually gets dialed.

* build: use portadialer.internal instead of webtrit.invalid for the decoy sip: host

Same RFC-reserved, never-resolving intent (RFC 9476 .internal instead
of RFC 2606 .invalid), just a more identifiable name for this address
in logs.

* build: remove dead emergency-number exception handling on Android

The sip: scheme change means Android's Telecom never classifies our
outgoing calls as emergency anymore, so the code path that used to
catch and report an emergency-number failure can never run:

- removed EmergencyNumberException and its throw/catch sites
- removed the now-single-case-only OutgoingFailureType.EMERGENCY_NUMBER
- removed the now-unused TelephonyUtils.isEmergencyNumber() helper

The shared PCallRequestErrorEnum.emergencyNumber /
CallkeepCallRequestError.emergencyNumber wire values are left in place
deliberately: the Kotlin side encodes this enum by explicit raw Int
values while the Dart side decodes by list index, so removing an entry
from the middle would require re-aligning every value after it by hand
on both sides to avoid a silent wire-format mismatch. Nothing on the
Android side sets this value anymore either way.

* build: centralize outgoing decoy sip: Uri construction in TelephonyUtils

* build: percent-encode the number in the outgoing sip: Uri

* build: bump version build iteration to 1.3.1+1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants