- 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>
179 lines
7.7 KiB
Markdown
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.
|