Files
jamie prince a31d6a693c Wear SIP: incoming call UI over lock screen via self-managed telecom
- Launch InCallActivity from WearConnectionService.onCreateIncomingConnection
  (self-managed telecom context) instead of a background FGS startActivity.
  This is the only path Android permits to show UI over a secure lock screen
  / from the background (BAL exemption); a plain FGS startActivity is blocked.
- Register a SELF_MANAGED PhoneAccount (CAPABILITY_SELF_MANAGED only; a
  self-managed account must not also be CALL_PROVIDER) and hand inbound calls
  to TelecomManager.addNewIncomingCall.
- Answer/Decline routed from the system self-managed Connection back to the
  native stack via WearBaresipServiceHelper (deferred ua_answer/ua_hangup).
- Add dedicated InCallActivity (setShowWhenLocked/turnScreenOn) hosting the
  InCallScreen; MainActivity no longer owns the call UI.
- Switch SIP transport to UDP:5761 (matches server); keep sipnat=outbound +
  natpinhole for NAT traversal.
- Remove dead phoneCall FGS upgrade/downgrade (needed DIALER role; the
  self-managed launch path replaces it).

Verified on Galaxy Watch (Android 16, SDK 36): inbound call rings + shows
InCallActivity over the lock screen (pattern lock on) with working two-way
audio; activity dismisses cleanly on hangup.
2026-08-21 03:15:22 +01:00
..

Baresip Wear — SIP client for Wear OS

A native SIP softphone for Wear OS watches, built on the baresip stack (via the libbaresip-android submodule). It registers a single SIP account, places/receives calls, and survives the watch's aggressive power management so inbound calls actually ring while the screen is off.

This app is a slimmed-down port of the full baresip Android app (app/ in this repo) to the watch form factor. It intentionally does not use the system telecom stack for call UI — the in-app InCallScreen plus a high-priority, full-screen-intent notification are the alert surface.


Current status (as of this commit)

Working:

  • Registration over UDP with NAT-survival settings (sipnat=outbound;natpinhole=yes in assets/accounts). This was the root cause of the original "call goes straight to voicemail" problem: without the pinhole the INVITE reply was dropped and Asterisk retransmit-timed-out.
  • Inbound calls ring whether the app is foreground or backgrounded, with vibration + screen wake.
  • Answer connects the call (caller stops hearing ringing; watch shows "Connected").
  • Remote hang-up terminates the call promptly whether answered or not (ring is cancelled, not left looping forever).
  • Foreground service is started from WearBaresipApp.onCreate() (decoupled from the Activity) and holds a WifiLock + WakeLock so SIP replies go out even when the screen is off / device is in a low-power state.
  • Lock-screen presentation attempted via manifest showWhenLocked / turnScreenOn + USE_FULL_SCREEN_INTENT + a full-screen-intent notification that launches MainActivityInCallScreen. (See "Known gaps".)

How answering actually works (important)

The native baresip library emits two events for an inbound call:

  1. incoming call,<peer>BEVENT_SIPSESS_CONN. We call Api.ua_accept() here so the caller hears ringing (100/180 provisional). The callp passed is the SIP message pointer carried by the event.
  2. call incoming,<prm>BEVENT_CALL_INCOMING. This carries the real, answerable struct call*. The user's Answer button calls Api.ua_answer(), which is deferred to the baresip main loop via a one-shot timer in baresip.c (ua_answer_async). This deferral is mandatory: calling ua_answer() directly from the UI thread crashes in sip_treplyf (SIGSEGV), because SIP transaction replies must run on the baresip thread. The native event callback (uaEvent) itself runs on the baresip thread, which is why ua_accept is safe there but ua_answer from a tap is not.

Key files:

  • wear/src/main/cpp/baresip.c — native event_handler (event strings + jlong ua/call args), ua_accept/ua_answer JNI, and the ua_answer_async main-loop hand-off.
  • wear/src/main/java/com/tutpro/baresip/wear/WearBaresipService.kt — the SIP event loop, alert/ring logic, stopRinging() + 30s ring watchdog, notification channels, and lock-screen flags.
  • wear/src/main/java/com/tutpro/baresip/wear/WearBaresipServiceHelper.kt — UI → native bridge (dial, answer, hangup, sendDigit).
  • wear/src/main/java/com/tutpro/baresip/wear/WearBaresipApp.kt — starts the foreground service from Application.onCreate() so it survives task removal.
  • wear/src/main/java/com/tutpro/baresip/wear/MainActivity.kt — Compose UI (dialer / in-call) + setShowWhenLocked/setTurnScreenOn for inbound calls.
  • wear/src/main/assets/accounts — the SIP account line (NAT settings + credentials).

Build & install

Prereqs: Android SDK, ndk (for the native build), and a watch with adb over Wi-Fi (wireless debugging). The native library is built by Gradle's external-native-build from wear/src/main/cpp/.

# From repo root
./gradlew :wear:assembleDebug

# Install to the watch (wireless ADB; port changes after a reboot/reconnect)
adb connect <watch-ip>:<port>
adb -s <watch-ip>:<port> install -r wear/build/outputs/apk/debug/wear-debug.apk

Note: a clean native rebuild is required after editing baresip.c:

./gradlew :wear:clean :wear:assembleDebug

Permissions to grant on-device

  • Microphone — record audio for calls.
  • Notifications (API 33+) — required for the incoming-call heads-up / full-screen intent to wake the watch face.
  • Phone / READ_PHONE_STATE — declared; needed if telecom integration is re-enabled.

Known gaps / bugs still open

  • Lock-screen Activity sometimes doesn't appear when the watch is fully asleep and the app is backgrounded. The vibration fires but the user has to open the app manually. The full-screen-intent notification + showWhenLocked are in place, but on Android 12+ (this watch runs Android 16) the system may still suppress the launch. Candidate fixes: ensure USE_FULL_SCREEN_INTENT is granted; raise the call notification to a dedicated InCallActivity (not routed through the LAUNCHER MainActivity); verify setShowWhenLocked(true) is applied before setContent in every launch path.
  • No DTMF / keypad verification end-to-end yet (wired via call_send_digit but not exercised on a real call).
  • No echo cancellation / audio-route tuning beyond the basic speaker-vs-BT-SCO switch.

Roadmap

Near-term (core completeness):

  • SIP MESSAGE (IM) support. baresip already has a message module. Add:
    • a native MESSAGE event → Kotlin bridge,
    • an inbox/thread UI (or at least a toast + unread badge),
    • outbound Api.message_send() from the dialer.
  • Read device contacts via the Wearable Data Layer / ContactsContract (the watch mirrors phone contacts through the companion app) so the dialer can autocomplete instead of requiring typed SIP URIs.
  • Start at boot / on network available. Register a BOOT_COMPLETED + CONNECTIVITY_CHANGED receiver that starts the foreground service so the watch is always reachable without the user launching the app first.
  • Call history / recent peers persisted to a small local store (DataStore/ Room) instead of the in-memory recentPeers list.

Medium-term (polish & robustness):

  • Better InCall UI: larger Answer/Decline hit targets, a proper "swipe to answer" gesture, mute/hold, and a clear connected timer.
  • Provisioning flow: ship the account config via a QR/baresip: URI or MDM rather than a hand-edited assets/accounts file.
  • Multiple accounts (currently a single default UA is assumed throughout WearBaresipServiceHelper).
  • Audio quality: AGC, echo cancellation, and BT-SCO reliability across headset brands.
  • Re-enable system telecom as an optional path behind a setting, now that the native answer/hangup path is stable — but keep the in-app UI as the default to avoid the earlier 2-second auto-drop.

Long-term:

  • Video calls (baresip supports it; watch camera is the constraint).
  • Encrypted messaging / OMEMO if the server supports it.
  • Wear companion app on the phone for config + contact sync.

Debugging tips

# Live native + Kotlin log during a call
adb -s <watch-ip>:<port> logcat -v time | grep -aE \
  'Baresip Wear|jni_ua|ua_answer|ua_accept|ua_hangup|F/DEBUG'

Watch for:

  • uaEvent: incoming call,... then ua_accept (caller starts ringing) then ua_answer ... deferred to main loopua_answer_async: donecall established.
  • A F/DEBUG SIGSEGV in sip_treplyf / call_answer means ua_answer or ua_accept was called from the wrong thread or with a bad handle.
  • fullScreenIntent=null in the notification dump means the system stripped the lock-screen launch — check USE_FULL_SCREEN_INTENT + notification channel importance.