Files
baresip-studio/wear/README.md
jamie prince 899e778e49 Wear SIP: stable inbound calls (ring/answer/hangup) + README
- NAT-survival account settings (sipnat=outbound;natpinhole=yes) so INVITE
  replies are not dropped (was going straight to voicemail).
- Native event_handler emits 'incoming call' (BEVENT_SIPSESS_CONN) carrying
  the SIP msg pointer and 'call incoming' (BEVENT_CALL_INCOMING) carrying the
  real struct call*; passes ua/call as separate jlong args (no lossy string
  parse). uaEvent(String, Long, Long) signature updated.
- Answer flow: ua_accept on 'incoming call' (caller hears ringing) then
  ua_answer deferred to the baresip main loop via a one-shot timer
  (ua_answer_async) so the UI-thread Answer tap does not crash in sip_treplyf.
- FGS started from Application.onCreate (decoupled from Activity) holding
  WifiLock + WakeLock so SIP survives screen-off / task removal.
- Ring: vibration + screen wake + high-priority full-screen-intent
  notification; stopRinging() on established/closed + 30s watchdog so the
  ring never loops forever on remote hang-up.
- Lock-screen presentation: manifest showWhenLocked/turnScreenOn +
  USE_FULL_SCREEN_INTENT permission + InCallScreen launch. (Still unreliable
  when fully asleep + backgrounded — tracked in README.)
- wear/README.md: project overview, how answering works, build/install,
  known gaps, and roadmap (SIP MESSAGE, contacts, start-at-boot, UI).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-21 01:58:56 +01:00

179 lines
7.7 KiB
Markdown

# Baresip Wear — SIP client for Wear OS
A native SIP softphone for Wear OS watches, built on the
[baresip](https://github.com/baresip/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 `MainActivity``InCallScreen`. (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/`.
```bash
# 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`:
```bash
./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
```bash
# 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 loop``ua_answer_async: done`
`call 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.