# 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) 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 : 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.