Skip to content

Commit 4e2af13

Browse files
fix(callkit): add voip background mode + mic permission so incoming calls work
Incoming voice calls were unusable: the CallKit screen flashed the caller's name, auto-dismissed, reappeared as 'Unknown', and couldn't be answered. Root cause: the app's UIBackgroundModes was missing 'voip', so callservicesd DENIED creation of the CallKit call source and reset the provider (providerDidReset → CallManager.handleCallKitReset → currentCallUUID=nil + callState=.ended). Found via the simulator's com.apple.calls.callkit unified log ('Denying creation of CXXPCCallSource … Not accepting connection'); the device diag.log only showed the effect (providerDidReset). - Info.plist: add 'voip' (+ 'audio') to UIBackgroundModes; add NSMicrophoneUsageDescription (the 'no audio' half — mic capture needs it). 'voip' is just an Info.plist key, no provisioning capability required. - Tests/interop/voice_caller.py: headless LXST caller (Sideband's ReticulumTelephone, wire-identical) that dials the sim's telephony dest to reproduce the incoming-call/CallKit path on the simulator — where CallKit runs, so the bug + the framework deny reason are observable (unlike on device). Verified on sim: with the fix, reportIncomingCall succeeds + callservicesd 'Created CXXPCCallSource' — no deny, no providerDidReset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f853b6d commit 4e2af13

2 files changed

Lines changed: 80 additions & 0 deletions

File tree

Sources/ColumbaApp/Resources/Info.plist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
<string>Columba shares your location with contacts you explicitly choose, so they can see where you are during an active sharing session.</string>
3030
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
3131
<string>Columba keeps sharing your location with the contacts you chose even when the app is in the background, so a sharing session you started doesn't silently stop. You can stop sharing at any time from the conversation or from Settings.</string>
32+
<key>NSMicrophoneUsageDescription</key>
33+
<string>Columba uses the microphone for encrypted voice calls over the mesh network.</string>
3234
<key>UIAppFonts</key>
3335
<array>
3436
<string>materialdesignicons.ttf</string>
@@ -42,9 +44,11 @@
4244
</dict>
4345
<key>UIBackgroundModes</key>
4446
<array>
47+
<string>audio</string>
4548
<string>bluetooth-central</string>
4649
<string>bluetooth-peripheral</string>
4750
<string>location</string>
51+
<string>voip</string>
4852
</array>
4953
<key>UILaunchScreen</key>
5054
<dict/>

Tests/interop/voice_caller.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env python3
2+
"""Headless LXST voice caller — dials the iOS Columba sim's telephony
3+
destination so its incoming-call / CallKit path fires, for debugging the
4+
"call screen dismisses + reappears as Unknown" bug on the sim (full log access).
5+
6+
Uses Sideband's own ReticulumTelephone (which wraps LXST.Telephone), so it's
7+
wire-identical to a Sideband user placing the call. Joins the host's shared
8+
RNS instance (lxmd) exactly like the interop peer, so it shares Columba's
9+
transport.
10+
11+
Usage:
12+
PYTHONPATH unneeded — paths injected below.
13+
python3 voice_caller.py <columba_identity_hex> [ring_seconds]
14+
"""
15+
import sys, os, time, threading
16+
17+
sys.path.insert(0, os.path.expanduser("~/repos/LXST"))
18+
sys.path.insert(0, os.path.expanduser("~/repos/Sideband"))
19+
20+
import RNS # noqa: E402
21+
from sbapp.sideband.voice import ReticulumTelephone # noqa: E402
22+
23+
COLUMBA_ID_HEX = sys.argv[1] if len(sys.argv) > 1 else "695f23533fe3547183f1b550d8552ae8"
24+
RING_SECONDS = int(sys.argv[2]) if len(sys.argv) > 2 else 25
25+
26+
27+
def main():
28+
RNS.Reticulum() # connect to the shared lxmd instance (share_instance=Yes)
29+
identity = RNS.Identity()
30+
print(f"[caller] my identity={RNS.prettyhexrep(identity.hash)}", flush=True)
31+
32+
phone = ReticulumTelephone(identity, owner=None)
33+
# NB: don't call phone.start() — its run() loop is Sideband hw-state polling
34+
# and isn't present on this build; the wrapped LXST.Telephone drives the
35+
# call protocol itself. We just need to keep the process alive.
36+
time.sleep(1.0)
37+
38+
phone.announce() # so Columba can resolve/path us (matches the 'correct name' case)
39+
print("[caller] announced own telephony", flush=True)
40+
41+
id_bytes = bytes.fromhex(COLUMBA_ID_HEX)
42+
tel_dest = RNS.Destination.hash_from_name_and_identity("lxst.telephony", id_bytes)
43+
print(f"[caller] Columba telephony dest = {tel_dest.hex()}", flush=True)
44+
45+
if not RNS.Transport.has_path(tel_dest):
46+
RNS.Transport.request_path(tel_dest)
47+
deadline = time.time() + 30
48+
while not RNS.Transport.has_path(tel_dest) and time.time() < deadline:
49+
time.sleep(0.5)
50+
if not RNS.Transport.has_path(tel_dest):
51+
print("[caller] NO PATH to Columba telephony after 30s — is the sim "
52+
"announcing lxst.telephony + bridged through lxmd? aborting.", flush=True)
53+
return 1
54+
print(f"[caller] path to telephony resolved ({RNS.Transport.hops_to(tel_dest)} hops); DIALING", flush=True)
55+
56+
result = phone.dial(id_bytes)
57+
print(f"[caller] dial() -> {result}", flush=True)
58+
59+
# state: 0=AVAILABLE 1=CONNECTING 2=RINGING 3=IN_CALL (is_* are @property, no parens)
60+
_names = {0: "AVAILABLE", 1: "CONNECTING", 2: "RINGING", 3: "IN_CALL"}
61+
for i in range(RING_SECONDS):
62+
st = phone.state
63+
print(f"[caller] t={i:>2}s state={st}({_names.get(st, '?')})", flush=True)
64+
time.sleep(1.0)
65+
66+
print("[caller] hanging up", flush=True)
67+
try:
68+
phone.hangup()
69+
except Exception as e:
70+
print(f"[caller] hangup error: {e}", flush=True)
71+
time.sleep(2.0)
72+
return 0
73+
74+
75+
if __name__ == "__main__":
76+
sys.exit(main())

0 commit comments

Comments
 (0)