Add BackHandler in WearApp: back from thread: -> messages, messages/accounts -> dialer. At the root dialer route the handler is disabled so back finishes the Activity normally. Fixes back-from-Messages dropping to the clock face.
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=yesinassets/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 aWifiLock+WakeLockso 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 launchesMainActivity→InCallScreen. (See "Known gaps".)
How answering actually works (important)
The native baresip library emits two events for an inbound call:
incoming call,<peer>—BEVENT_SIPSESS_CONN. We callApi.ua_accept()here so the caller hears ringing (100/180 provisional). Thecallppassed is the SIP message pointer carried by the event.call incoming,<prm>—BEVENT_CALL_INCOMING. This carries the real, answerablestruct call*. The user's Answer button callsApi.ua_answer(), which is deferred to the baresip main loop via a one-shot timer inbaresip.c(ua_answer_async). This deferral is mandatory: callingua_answer()directly from the UI thread crashes insip_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 whyua_acceptis safe there butua_answerfrom a tap is not.
Key files:
wear/src/main/cpp/baresip.c— nativeevent_handler(event strings +jlongua/call args),ua_accept/ua_answerJNI, and theua_answer_asyncmain-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 fromApplication.onCreate()so it survives task removal.wear/src/main/java/com/tutpro/baresip/wear/MainActivity.kt— Compose UI (dialer / in-call) +setShowWhenLocked/setTurnScreenOnfor 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 +
showWhenLockedare in place, but on Android 12+ (this watch runs Android 16) the system may still suppress the launch. Candidate fixes: ensureUSE_FULL_SCREEN_INTENTis granted; raise the call notification to a dedicatedInCallActivity(not routed through the LAUNCHERMainActivity); verifysetShowWhenLocked(true)is applied beforesetContentin every launch path. - No DTMF / keypad verification end-to-end yet (wired via
call_send_digitbut 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 + amessage_listenreceive callback (message_handlerinbaresip.c) forwarding toWearBaresipService.messageEvent. - Kotlin:
Api.message_send,WearBaresipServiceHelper.sendMessage(normalizes peer →sip:peer@mail.txt3.net),MessageStore(JSON file infilesDir), andMainActivityMessagesScreen/ConversationScreen(conversation list + thread + send field, reached via a "Messages" button on the dialer). - Incoming messages are persisted and surfaced via a
CATEGORY_MESSAGEnotification. - 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).
- Native:
- 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_CHANGEDreceiver 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
recentPeerslist.
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-editedassets/accountsfile. - 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.soloaded inconfig.static). Verify SRTP actually negotiates on a TLS:5062 call: capture the SDP and confirma=crypto(SDES) ora=fingerprint(DTLS-SRTP) is offered/answered. Then pair TLS signalling + DTLS-SRTP for fully encrypted calls (signalling + media). May need the Asterisk side setencryption=yes/media_encryption=dtls(orsdes). - WebSocket (WS/WSS) transport support — baresip core supports
transport=ws/wssand the registrar filter already allowsws,wss, but thewebsocketmodule is not compiled intolibbaresip.afor this wear build (onlysrtp/dtls_srtp/gzrtpmodules are present; nowebsocket/module). To enable: add thewebsocketmodule to the baresip build, rebuildlibbaresip.a+libwearbaresip.so, load it inconfig.static, and useoutbound="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
# 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,...thenua_accept(caller starts ringing) thenua_answer ... deferred to main loop→ua_answer_async: done→call established.- A
F/DEBUGSIGSEGV insip_treplyf/call_answermeansua_answerorua_acceptwas called from the wrong thread or with a bad handle. fullScreenIntent=nullin the notification dump means the system stripped the lock-screen launch — checkUSE_FULL_SCREEN_INTENT+ notification channel importance.