# 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,` — `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,` — `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 : adb -s : 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) — DONE in this branch.** Plaintext RFC 3428 MESSAGE is implemented end-to-end: - Native: `message_send()` JNI + a `message_listen` receive callback (`message_handler` in `baresip.c`) forwarding to `WearBaresipService.messageEvent`. - Kotlin: `Api.message_send`, `WearBaresipServiceHelper.sendMessage` (normalizes peer → `sip:peer@mail.txt3.net`), `MessageStore` (JSON file in `filesDir`), and `MainActivity` `MessagesScreen` / `ConversationScreen` (conversation list + thread + send field, reached via a "Messages" button on the dialer). - Incoming messages are persisted and surfaced via a `CATEGORY_MESSAGE` notification. - **Encryption is NOT yet in this pass** — see the long-term item; the body is sent in cleartext (protected only by the TLS SIP transport in transit). - **Encrypted SIP MESSAGE (next phase).** Plan agreed: **asymmetric E2E** using a per-device X25519/Ed25519 keypair, with public keys exchanged **out-of-band via NFC tap** (strongest trust — no server in the path), falling back to exchange over a plaintext MESSAGE if NFC is unavailable (trusts the TLS server). The MESSAGE body is then encrypted with the peer's public key before `message_send`. Note: **OMEMO does not apply here** — OMEMO is an XMPP (Jabber) standard and cannot run over SIP; we implement the same *class* of protection (Signal-style envelope) on top of SIP MESSAGE instead. OpenSSL (already bundled) provides the crypto. - **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. - **SRTP media encryption** — already compiled in (`srtp.so`, `dtls_srtp.so`, `gzrtp.so` loaded in `config.static`). Verify SRTP actually negotiates on a TLS:5062 call: capture the SDP and confirm `a=crypto` (SDES) or `a=fingerprint` (DTLS-SRTP) is offered/answered. Then pair TLS signalling + DTLS-SRTP for fully encrypted calls (signalling + media). May need the Asterisk side set `encryption=yes` / `media_encryption=dtls` (or `sdes`). - **WebSocket (WS/WSS) transport support** — baresip core supports `transport=ws`/`wss` and the registrar filter already allows `ws,wss`, but the `websocket` module is **not** compiled into `libbaresip.a` for this wear build (only `srtp`/`dtls_srtp`/`gzrtp` modules are present; no `websocket/` module). To enable: add the `websocket` module to the baresip build, rebuild `libbaresip.a` + `libwearbaresip.so`, load it in `config.static`, and use `outbound="sip:host:PORT;transport=wss"`. Only needed if the server fronts SIP behind WebSocket (WebRTC/CPaaS-style), not typical for an Asterisk PBX. Long-term: - **Video calls** (baresip supports it; watch camera is the constraint). - **Wear companion app** on the phone for config + contact sync. --- ## Debugging tips ```bash # Live native + Kotlin log during a call adb -s : 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.