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>
This commit is contained in:
178
wear/README.md
Normal file
178
wear/README.md
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
# 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.
|
||||||
@ -11,6 +11,7 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||||
@ -29,7 +30,9 @@
|
|||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="Baresip"
|
android:label="Baresip"
|
||||||
android:taskAffinity="">
|
android:taskAffinity=""
|
||||||
|
android:showWhenLocked="true"
|
||||||
|
android:turnScreenOn="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
<sip:01273961147@mail.txt3.net>;auth_user="01273961147";auth_pass="cisco55555";outbound="sip:mail.txt3.net:5060;transport=tcp";regint=60;regq=0.5;pubint=0;check_origin=no;mwi=no;natpinhole=yes
|
<sip:01273961147@mail.txt3.net>;auth_user="01273961147";auth_pass="cisco55555";outbound="sip:mail.txt3.net:5060;transport=tcp";regint=60;regq=0.5;pubint=0;check_origin=no;mwi=no;sipnat=outbound;natpinhole=yes
|
||||||
|
|
||||||
|
|||||||
@ -115,13 +115,18 @@ static void event_handler(enum bevent_ev ev, struct bevent *event, void *arg)
|
|||||||
len = re_snprintf(event_buf, sizeof event_buf, "registering failed,%ld", (long)ua);
|
len = re_snprintf(event_buf, sizeof event_buf, "registering failed,%ld", (long)ua);
|
||||||
break;
|
break;
|
||||||
case BEVENT_SIPSESS_CONN:
|
case BEVENT_SIPSESS_CONN:
|
||||||
|
// Inbound-INVITE precursor: no call object yet. The `call` slot
|
||||||
|
// (passed to Kotlin as the 3rd uaEvent arg) holds the SIP message
|
||||||
|
// pointer, which is the correct handle for ua_accept(). Do NOT
|
||||||
|
// overwrite it with bevent_get_call() (garbage at this stage).
|
||||||
ua = uag_find_msg(msg);
|
ua = uag_find_msg(msg);
|
||||||
call = (struct call *)msg;
|
call = (struct call *)msg;
|
||||||
len = re_snprintf(event_buf, sizeof event_buf, "%s,%r,%ld", prm, &msg->from.auri, (long)event);
|
len = re_snprintf(event_buf, sizeof event_buf, "incoming call,%r",
|
||||||
|
&msg->from.auri);
|
||||||
break;
|
break;
|
||||||
case BEVENT_CALL_INCOMING:
|
case BEVENT_CALL_INCOMING:
|
||||||
len = re_snprintf(event_buf, sizeof event_buf, "call incoming,%s,%ld,%ld",
|
len = re_snprintf(event_buf, sizeof event_buf, "call incoming,%s",
|
||||||
prm, (long)ua, (long)call);
|
prm);
|
||||||
break;
|
break;
|
||||||
case BEVENT_CALL_OUTGOING:
|
case BEVENT_CALL_OUTGOING:
|
||||||
len = re_snprintf(event_buf, sizeof event_buf, "call outgoing", "");
|
len = re_snprintf(event_buf, sizeof event_buf, "call outgoing", "");
|
||||||
@ -201,10 +206,15 @@ static void event_handler(enum bevent_ev ev, struct bevent *event, void *arg)
|
|||||||
if (!g_ctx.serviceClz || !g_ctx.serviceObj) return;
|
if (!g_ctx.serviceClz || !g_ctx.serviceObj) return;
|
||||||
|
|
||||||
jmethodID methodId =
|
jmethodID methodId =
|
||||||
(*env)->GetMethodID(env, g_ctx.serviceClz, "uaEvent", "(Ljava/lang/String;)V");
|
(*env)->GetMethodID(env, g_ctx.serviceClz, "uaEvent", "(Ljava/lang/String;JJ)V");
|
||||||
jstring jEvent = (*env)->NewStringUTF(env, event_buf);
|
jstring jEvent = (*env)->NewStringUTF(env, event_buf);
|
||||||
if (methodId) {
|
if (methodId) {
|
||||||
(*env)->CallVoidMethod(env, g_ctx.serviceObj, methodId, jEvent);
|
// Pass ua and call as separate native args (mirrors the upstream
|
||||||
|
// baresip Android app). At BEVENT_SIPSESS_CONN, `call` holds the SIP
|
||||||
|
// message pointer (set explicitly above), which is the correct handle
|
||||||
|
// for ua_accept(). Embedding pointers in the event string is lossy.
|
||||||
|
(*env)->CallVoidMethod(env, g_ctx.serviceObj, methodId, jEvent,
|
||||||
|
(jlong)ua, (jlong)call);
|
||||||
if ((*env)->ExceptionCheck(env)) {
|
if ((*env)->ExceptionCheck(env)) {
|
||||||
(*env)->ExceptionDescribe(env);
|
(*env)->ExceptionDescribe(env);
|
||||||
(*env)->ExceptionClear(env);
|
(*env)->ExceptionClear(env);
|
||||||
@ -414,14 +424,83 @@ static void jni_ua_hangup(jlong uap, jlong callp, jint code, const char *reason)
|
|||||||
re_thread_leave();
|
re_thread_leave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void ua_answer_async(void *arg);
|
||||||
|
|
||||||
static void jni_ua_answer(jlong uap, jlong callp, jint video)
|
static void jni_ua_answer(jlong uap, jlong callp, jint video)
|
||||||
{
|
{
|
||||||
struct ua *ua = (struct ua *)(intptr_t)uap;
|
struct ua *ua = (struct ua *)(intptr_t)uap;
|
||||||
struct call *call = (struct call *)(intptr_t)callp;
|
struct call *call = (struct call *)(intptr_t)callp;
|
||||||
|
|
||||||
re_thread_enter();
|
LOGD("jni_ua_answer: ua=%ld call=%ld video=%d\n", (long)ua, (long)call, video);
|
||||||
|
|
||||||
|
// ua_answer() must run on the baresip main-loop thread (it drives SIP
|
||||||
|
// transactions via sip_treply). Calling it from the UI thread (an Answer
|
||||||
|
// tap) crashes in sip_treplyf. Hand it off to the main loop via a timer
|
||||||
|
// scheduled in the immediate past, so re_main() runs it on the right
|
||||||
|
// thread. We hold the handles with mem_ref and release them afterwards.
|
||||||
|
struct mbuf *mb = mbuf_alloc(sizeof(uint32_t) * 3);
|
||||||
|
if (!mb) {
|
||||||
|
LOGW("jni_ua_answer: mbuf_alloc failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ua) mem_ref(ua);
|
||||||
|
if (call) mem_ref(call);
|
||||||
|
mbuf_write_u32(mb, (uint32_t)(uintptr_t)ua);
|
||||||
|
mbuf_write_u32(mb, (uint32_t)(uintptr_t)call);
|
||||||
|
mbuf_write_u32(mb, (uint32_t)video);
|
||||||
|
mb->pos = 0;
|
||||||
|
|
||||||
|
struct tmr *t = mem_zalloc(sizeof(*t), NULL);
|
||||||
|
if (!t) {
|
||||||
|
LOGW("jni_ua_answer: tmr alloc failed\n");
|
||||||
|
mem_deref(mb);
|
||||||
|
if (ua) mem_deref(ua);
|
||||||
|
if (call) mem_deref(call);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tmr_start(t, 1, ua_answer_async, mb);
|
||||||
|
LOGD("jni_ua_answer: deferred to main loop\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ua_answer_async(void *arg)
|
||||||
|
{
|
||||||
|
struct mbuf *mb = arg;
|
||||||
|
struct ua *ua;
|
||||||
|
struct call *call;
|
||||||
|
int video;
|
||||||
|
|
||||||
|
if (!mb) return;
|
||||||
|
ua = (struct ua *)(uintptr_t)mbuf_read_u32(mb);
|
||||||
|
call = (struct call *)(uintptr_t)mbuf_read_u32(mb);
|
||||||
|
video = (int)mbuf_read_u32(mb);
|
||||||
|
|
||||||
|
LOGD("ua_answer_async: ua=%ld call=%ld\n", (long)ua, (long)call);
|
||||||
ua_answer(ua, call, (enum vidmode)video);
|
ua_answer(ua, call, (enum vidmode)video);
|
||||||
|
LOGD("ua_answer_async: done\n");
|
||||||
|
|
||||||
|
if (ua) mem_deref(ua);
|
||||||
|
if (call) mem_deref(call);
|
||||||
|
mem_deref(mb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept an inbound call from its SIPSESS_CONN handle (the bevent pointer,
|
||||||
|
// which holds the sip_msg*). ua_accept() allocates the real call from the
|
||||||
|
// message, so this is the correct answer entry point for inbound calls --
|
||||||
|
// ua_answer() requires an already-allocated struct call* and crashes (SIGSEGV
|
||||||
|
// in call_answer) when handed the precursor message pointer.
|
||||||
|
static void jni_ua_accept(jlong uap, jlong msgp)
|
||||||
|
{
|
||||||
|
struct ua *ua = (struct ua *)(intptr_t)uap;
|
||||||
|
struct sip_msg *msg = (struct sip_msg *)(intptr_t)msgp;
|
||||||
|
int err;
|
||||||
|
|
||||||
|
LOGD("jni_ua_accept: ua=%ld msg=%ld\n", (long)ua, (long)msg);
|
||||||
|
re_thread_enter();
|
||||||
|
err = ua_accept(ua, msg);
|
||||||
re_thread_leave();
|
re_thread_leave();
|
||||||
|
LOGD("jni_ua_accept: result=%d\n", err);
|
||||||
|
if (err)
|
||||||
|
LOGW("ua_accept failed: %d\n", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
static jlong jni_call_connect(jlong callp, const char *peer_uri)
|
static jlong jni_call_connect(jlong callp, const char *peer_uri)
|
||||||
@ -517,6 +596,15 @@ Java_com_tutpro_baresip_wear_Api_ua_1answer(JNIEnv *env, jclass clazz,
|
|||||||
jni_ua_answer(uap, callp, video);
|
jni_ua_answer(uap, callp, video);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_tutpro_baresip_wear_Api_ua_1accept(JNIEnv *env, jclass clazz,
|
||||||
|
jlong uap, jlong msgp)
|
||||||
|
{
|
||||||
|
(void)env;
|
||||||
|
(void)clazz;
|
||||||
|
jni_ua_accept(uap, msgp);
|
||||||
|
}
|
||||||
|
|
||||||
JNIEXPORT jlong JNICALL
|
JNIEXPORT jlong JNICALL
|
||||||
Java_com_tutpro_baresip_wear_Api_call_1connect(JNIEnv *env, jclass clazz,
|
Java_com_tutpro_baresip_wear_Api_call_1connect(JNIEnv *env, jclass clazz,
|
||||||
jlong callp, jstring jPeer)
|
jlong callp, jstring jPeer)
|
||||||
|
|||||||
@ -11,6 +11,11 @@ class Api private constructor() {
|
|||||||
@JvmStatic external fun ua_register(uap: Long): Int
|
@JvmStatic external fun ua_register(uap: Long): Int
|
||||||
@JvmStatic external fun ua_hangup(uap: Long, callp: Long, code: Int, reason: String)
|
@JvmStatic external fun ua_hangup(uap: Long, callp: Long, code: Int, reason: String)
|
||||||
@JvmStatic external fun ua_answer(uap: Long, callp: Long, video: Int)
|
@JvmStatic external fun ua_answer(uap: Long, callp: Long, video: Int)
|
||||||
|
// Accept an inbound call from its SIPSESS_CONN handle (the bevent
|
||||||
|
// pointer, which holds the SIP message). ua_accept() allocates the
|
||||||
|
// real call internally -- this is the correct answer entry point for
|
||||||
|
// inbound calls (ua_answer needs an already-allocated call pointer).
|
||||||
|
@JvmStatic external fun ua_accept(uap: Long, msgp: Long)
|
||||||
@JvmStatic external fun ua_call_alloc(uap: Long, xcallp: Long, video: Int): Long
|
@JvmStatic external fun ua_call_alloc(uap: Long, xcallp: Long, video: Int): Long
|
||||||
@JvmStatic external fun call_connect(callp: Long, peerUri: String): Int
|
@JvmStatic external fun call_connect(callp: Long, peerUri: String): Int
|
||||||
@JvmStatic external fun call_hold(callp: Long, hold: Boolean): Boolean
|
@JvmStatic external fun call_hold(callp: Long, hold: Boolean): Boolean
|
||||||
|
|||||||
@ -33,5 +33,5 @@ object CallState {
|
|||||||
|
|
||||||
fun active() = calls.firstOrNull { it.status == "call established" || it.status == "call outgoing" || it.status == "call ringing" }
|
fun active() = calls.firstOrNull { it.status == "call established" || it.status == "call outgoing" || it.status == "call ringing" }
|
||||||
|
|
||||||
fun incoming() = calls.firstOrNull { it.status == "call incoming" }
|
fun incoming() = calls.firstOrNull { it.status == "incoming call" }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -443,6 +443,7 @@ fun AccountsScreen(onBack: () -> Unit) {
|
|||||||
append(";outbound=\"$out\"")
|
append(";outbound=\"$out\"")
|
||||||
append(";stunserver=\"stun:stun.l.google.com:19302\"")
|
append(";stunserver=\"stun:stun.l.google.com:19302\"")
|
||||||
append(";regq=0.5;pubint=0;check_origin=no;mwi=no")
|
append(";regq=0.5;pubint=0;check_origin=no;mwi=no")
|
||||||
|
append(";sipnat=outbound;natpinhole=yes")
|
||||||
if (regInt.isNotBlank()) append(";regint=$regInt")
|
if (regInt.isNotBlank()) append(";regint=$regInt")
|
||||||
}
|
}
|
||||||
File(filesDir, "accounts").writeText("$accountLine\n", Charsets.UTF_8)
|
File(filesDir, "accounts").writeText("$accountLine\n", Charsets.UTF_8)
|
||||||
@ -530,7 +531,7 @@ fun InCallScreen(onEnd: () -> Unit) {
|
|||||||
|
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
if (call != null && call.status == "call incoming") {
|
if (call != null && call.status == "incoming call") {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Button(onClick = { answerCall(call) }) {
|
Button(onClick = { answerCall(call) }) {
|
||||||
Icon(Icons.Filled.Call, contentDescription = "Answer")
|
Icon(Icons.Filled.Call, contentDescription = "Answer")
|
||||||
|
|||||||
@ -1,10 +1,31 @@
|
|||||||
package com.tutpro.baresip.wear
|
package com.tutpro.baresip.wear
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
class WearBaresipApp : Application() {
|
class WearBaresipApp : Application() {
|
||||||
// The foreground service is launched from MainActivity (foreground state),
|
// Start the SIP foreground service from the Application context so it is
|
||||||
// not from Application.onCreate(), because starting a microphone-type
|
// tied to the PROCESS lifetime, not the MainActivity task. On Wear OS a
|
||||||
// foreground service from a non-foreground context is rejected on
|
// foreground service launched only from an Activity gets recycled when the
|
||||||
// targetSdk 34+.
|
// Activity task is removed / screen goes off (the service's onDestroy ->
|
||||||
|
// baresipStop() tears down registration and we miss incoming calls).
|
||||||
|
//
|
||||||
|
// A microphone-type FGS is one of the types explicitly permitted for
|
||||||
|
// background start on API 34+, so this does not trip the
|
||||||
|
// "startForegroundService() not allowed in background" SecurityException
|
||||||
|
// that the old phoneCall-type start hit.
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
try {
|
||||||
|
val intent = Intent(this, WearBaresipService::class.java)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
startService(intent)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.w("Baresip Wear", "app start FGS failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,62 @@ import java.net.NetworkInterface
|
|||||||
|
|
||||||
class WearBaresipService : Service() {
|
class WearBaresipService : Service() {
|
||||||
|
|
||||||
|
// Keep the WiFi radio (and CPU) alive so the SIP stack can send the
|
||||||
|
// 100/180/200 responses to an inbound INVITE even when the watch screen
|
||||||
|
// is off / device is in a low-power state. Without these, the INVITE is
|
||||||
|
// received but the reply is dropped (or delayed past Asterisk's 6.4s
|
||||||
|
// retransmit timeout) and the call falls through to voicemail.
|
||||||
|
private var wifiLock: android.net.wifi.WifiManager.WifiLock? = null
|
||||||
|
private var wakeLock: android.os.PowerManager.WakeLock? = null
|
||||||
|
// Held while the call is ringing so we can cancel it the instant the call
|
||||||
|
// is answered / declined / remotely dropped. Without this the ring tone
|
||||||
|
// (a looping vibration pattern) never stops.
|
||||||
|
private var ringVibrator: Vibrator? = null
|
||||||
|
// Main-thread handler for the ring watchdog (postDelayed).
|
||||||
|
private val watchdogHandler = Handler(Looper.getMainLooper())
|
||||||
|
// Tracks the live call's native handle for hangup/decline. Updated on
|
||||||
|
// incoming/established/closed events. A single call at a time on the watch.
|
||||||
|
private var activeCallp: Long = 0L
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun acquireWifiLock() {
|
||||||
|
try {
|
||||||
|
val wm = getSystemService(android.net.wifi.WifiManager::class.java)
|
||||||
|
if (wm != null) {
|
||||||
|
wifiLock = wm.createWifiLock(
|
||||||
|
android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF,
|
||||||
|
"BaresipWear:SipWifi"
|
||||||
|
).apply {
|
||||||
|
setReferenceCounted(false)
|
||||||
|
if (!isHeld) acquire()
|
||||||
|
}
|
||||||
|
Log.d("Baresip Wear", "wifi lock acquired: ${wifiLock?.isHeld}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("Baresip Wear", "acquireWifiLock failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
private fun acquireWakeLock(millis: Long) {
|
||||||
|
try {
|
||||||
|
val pm = getSystemService(android.os.PowerManager::class.java)
|
||||||
|
if (pm != null) {
|
||||||
|
wakeLock = pm.newWakeLock(
|
||||||
|
android.os.PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
|
"BaresipWear:CallWake"
|
||||||
|
).apply {
|
||||||
|
setReferenceCounted(false)
|
||||||
|
if (!isHeld) acquire(millis)
|
||||||
|
}
|
||||||
|
Log.d("Baresip Wear", "wake lock acquired for ${millis}ms: ${wakeLock?.isHeld}")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("Baresip Wear", "acquireWakeLock failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
appContext = applicationContext
|
appContext = applicationContext
|
||||||
@ -67,8 +123,13 @@ class WearBaresipService : Service() {
|
|||||||
.setContentTitle("Baresip Wear")
|
.setContentTitle("Baresip Wear")
|
||||||
.setContentText("SIP stack running")
|
.setContentText("SIP stack running")
|
||||||
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
.build()
|
.build()
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
|
// Hold the WiFi radio + CPU so inbound INVITEs can be answered even
|
||||||
|
// when the screen is off.
|
||||||
|
acquireWifiLock()
|
||||||
|
|
||||||
val path = applicationContext.filesDir.absolutePath
|
val path = applicationContext.filesDir.absolutePath
|
||||||
val nativeLibDir = applicationInfo.nativeLibraryDir
|
val nativeLibDir = applicationInfo.nativeLibraryDir
|
||||||
@ -156,7 +217,17 @@ class WearBaresipService : Service() {
|
|||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
|
// Keep the SIP stack alive even if the MainActivity task is swiped
|
||||||
|
// away. Returning (not calling stopSelf) lets the FGS keep running and
|
||||||
|
// keep the registration / incoming-call path alive. The manifest also
|
||||||
|
// sets stopWithTask=false as a belt-and-braces measure.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
try { wifiLock?.let { if (it.isHeld) it.release() } } catch (_: Exception) {}
|
||||||
|
try { wakeLock?.let { if (it.isHeld) it.release() } } catch (_: Exception) {}
|
||||||
try { unregisterReceiver(networkReceiver) } catch (_: Exception) {}
|
try { unregisterReceiver(networkReceiver) } catch (_: Exception) {}
|
||||||
AudioRouteManager.shutdown()
|
AudioRouteManager.shutdown()
|
||||||
baresipStop()
|
baresipStop()
|
||||||
@ -189,22 +260,16 @@ class WearBaresipService : Service() {
|
|||||||
return addrs.joinToString(";")
|
return addrs.joinToString(";")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun uaEvent(event: String) {
|
fun uaEvent(event: String, uap: Long, callp: Long) {
|
||||||
val parts = event.split(",")
|
val parts = event.split(",")
|
||||||
val ev = parts[0]
|
val ev = parts[0]
|
||||||
val arg = parts.getOrNull(1) ?: ""
|
val arg = parts.getOrNull(1) ?: ""
|
||||||
// Event formats differ by event type (see baresip.c event_handler):
|
// uap/callp are passed as native args by baresip.c (mirrors the
|
||||||
// registering/registered/unregistering : "<ev>,<uap>" -> uap in parts[1]
|
// upstream baresip Android app). `callp` at "incoming call" holds the
|
||||||
// call incoming/outgoing/established.. : "<ev>,<peer>,<uap>,<callp>" -> uap in parts[2]
|
// SIP message pointer for ua_accept(); for other events it is the real
|
||||||
// Reading a fixed index breaks registration (uap lands in parts[1]
|
// struct call* from bevent_get_call(). Do not re-parse pointers from
|
||||||
// but we read parts[2]) -> defaultUap stays 0 -> dial() bails.
|
// the string -- that is lossy and was crashing ua_answer/ua_accept.
|
||||||
val uap = if (ev.startsWith("register") || ev.startsWith("unregister") || ev == "create") {
|
android.util.Log.d("Baresip Wear", "uaEvent: $event (uap=$uap callp=$callp)")
|
||||||
parts.getOrNull(1)?.toLongOrNull() ?: 0L
|
|
||||||
} else {
|
|
||||||
parts.getOrNull(2)?.toLongOrNull() ?: 0L
|
|
||||||
}
|
|
||||||
val callp = parts.getOrNull(3)?.toLongOrNull() ?: 0L
|
|
||||||
android.util.Log.d("Baresip Wear", "uaEvent: $event (uap=$uap)")
|
|
||||||
|
|
||||||
when {
|
when {
|
||||||
ev.startsWith("registering") -> {
|
ev.startsWith("registering") -> {
|
||||||
@ -221,35 +286,55 @@ class WearBaresipService : Service() {
|
|||||||
}
|
}
|
||||||
ev.startsWith("unregistering") -> CallState.registration.value = "Unregistered"
|
ev.startsWith("unregistering") -> CallState.registration.value = "Unregistered"
|
||||||
|
|
||||||
ev == "call incoming" -> {
|
// "incoming call,<peer>" is emitted at BEVENT_SIPSESS_CONN. Accept
|
||||||
|
// the session here (ua_accept) so the caller hears ringing
|
||||||
|
// (100/180). The actual answer (200 OK) happens when the user
|
||||||
|
// taps the Answer button (WearBaresipServiceHelper.answer), which
|
||||||
|
// is deferred to the baresip main loop for thread safety.
|
||||||
|
ev == "incoming call" -> {
|
||||||
val peer = arg.ifEmpty { "unknown" }
|
val peer = arg.ifEmpty { "unknown" }
|
||||||
val cp = if (callp != 0L) callp else peer.hashCode().toLong()
|
val cp = if (callp != 0L) callp else peer.hashCode().toLong()
|
||||||
if (CallState.find(cp) == null) {
|
if (CallState.find(cp) == null) {
|
||||||
CallState.add(WearCall(cp, uap, peer, "call incoming", "in"))
|
CallState.add(WearCall(cp, uap, peer, "incoming call", "in"))
|
||||||
}
|
}
|
||||||
CallState.status.value = "Incoming call"
|
CallState.status.value = "Incoming call"
|
||||||
// Surface the incoming-call UI even if the app was backgrounded,
|
|
||||||
// and alert the wearer (the watch screen is usually dimmed).
|
|
||||||
// NOTE: we deliberately do NOT hand the call to the system
|
|
||||||
// telecom stack. On Samsung Wear the third-party COMPANION
|
|
||||||
// PhoneAccount is auto-rejected within ~2s, and that rejection
|
|
||||||
// propagated back to the native baresip call (hangup) — which
|
|
||||||
// made Asterisk drop to voicemail immediately instead of ringing
|
|
||||||
// for the configured 25s. The in-app InCallScreen + the
|
|
||||||
// full-screen-intent notification below are the real alert UI.
|
|
||||||
alertIncomingCall(peer)
|
alertIncomingCall(peer)
|
||||||
|
if (callp != 0L) {
|
||||||
|
WearBaresipService.activeCallp = callp
|
||||||
|
Log.d("Baresip Wear", "ua_accept uap=$uap msgp=$callp")
|
||||||
|
Api.ua_accept(uap, callp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// BEVENT_CALL_INCOMING carries the real answerable struct call*.
|
||||||
|
// Track it so the Answer button can answer it. We do NOT answer
|
||||||
|
// here -- the user answers via the button (deferred to main loop).
|
||||||
|
ev == "call incoming" -> {
|
||||||
|
if (callp != 0L) {
|
||||||
|
WearBaresipService.activeCallp = callp
|
||||||
|
Log.d("Baresip Wear", "call incoming tracked callp=$callp")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ev == "call outgoing" -> CallState.status.value = "Calling"
|
ev == "call outgoing" -> CallState.status.value = "Calling"
|
||||||
ev == "call established" -> {
|
ev == "call established" -> {
|
||||||
CallState.status.value = "Connected"
|
CallState.status.value = "Connected"
|
||||||
CallState.active()?.status = "call established"
|
CallState.active()?.status = "call established"
|
||||||
|
if (callp != 0L) WearBaresipService.activeCallp = callp
|
||||||
|
// Stop the ring vibration/alert; the call is now answered.
|
||||||
|
stopRinging()
|
||||||
|
// Sustain the wake lock through the active call so RTP/audio
|
||||||
|
// keep flowing even if the screen dims.
|
||||||
|
acquireWakeLock(60_000L)
|
||||||
// Route audio: speaker if no BT headset, else BT SCO.
|
// Route audio: speaker if no BT headset, else BT SCO.
|
||||||
AudioRouteManager.startCallAudio()
|
AudioRouteManager.startCallAudio()
|
||||||
}
|
}
|
||||||
ev == "call ringing" -> CallState.status.value = "Ringing"
|
ev == "call ringing" -> CallState.status.value = "Ringing"
|
||||||
ev == "call closed" -> {
|
ev == "call closed" -> {
|
||||||
AudioRouteManager.stopCallAudio()
|
AudioRouteManager.stopCallAudio()
|
||||||
|
stopRinging()
|
||||||
dismissIncomingCallNotification()
|
dismissIncomingCallNotification()
|
||||||
|
WearBaresipService.activeCallp = 0L
|
||||||
|
// Release any call wake lock now that the call is over.
|
||||||
|
try { wakeLock?.let { if (it.isHeld) it.release() } } catch (_: Exception) {}
|
||||||
// Surface SIP failure reasons (e.g. "488 Not acceptable here")
|
// Surface SIP failure reasons (e.g. "488 Not acceptable here")
|
||||||
// as a toast so the user isn't left guessing why the call died.
|
// as a toast so the user isn't left guessing why the call died.
|
||||||
if (arg.isNotBlank()) showToast("Call ended: $arg")
|
if (arg.isNotBlank()) showToast("Call ended: $arg")
|
||||||
@ -259,7 +344,7 @@ class WearBaresipService : Service() {
|
|||||||
else -> CallState.status.value = ev
|
else -> CallState.status.value = ev
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.isNotEmpty() && (ev == "call incoming" || ev == "call outgoing") &&
|
if (arg.isNotEmpty() && (ev == "incoming call" || ev == "call outgoing") &&
|
||||||
!CallState.recentPeers.contains(arg)
|
!CallState.recentPeers.contains(arg)
|
||||||
) {
|
) {
|
||||||
CallState.recentPeers.add(0, arg)
|
CallState.recentPeers.add(0, arg)
|
||||||
@ -282,6 +367,9 @@ class WearBaresipService : Service() {
|
|||||||
* We also wake the (usually dimmed) screen and buzz a ring pattern.
|
* We also wake the (usually dimmed) screen and buzz a ring pattern.
|
||||||
*/
|
*/
|
||||||
private fun alertIncomingCall(peer: String) {
|
private fun alertIncomingCall(peer: String) {
|
||||||
|
// Keep the CPU awake long enough to send the SIP 180/200 and present
|
||||||
|
// the heads-up. Released when the call ends.
|
||||||
|
acquireWakeLock(60_000L)
|
||||||
// Wake the screen if it's asleep.
|
// Wake the screen if it's asleep.
|
||||||
try {
|
try {
|
||||||
val pm = getSystemService(PowerManager::class.java)
|
val pm = getSystemService(PowerManager::class.java)
|
||||||
@ -344,9 +432,14 @@ class WearBaresipService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ring vibration pattern (vibrate, pause, vibrate, pause, ...).
|
// Ring vibration pattern (vibrate, pause, vibrate, pause, ...).
|
||||||
|
// Repeat index 0 = loop while the call is incoming. stopRinging()
|
||||||
|
// cancels it on answer / decline / remote hangup. A watchdog below
|
||||||
|
// also force-stops it if the call-event never arrives (e.g. a remote
|
||||||
|
// CANCEL that baresip doesn't surface as "call closed").
|
||||||
try {
|
try {
|
||||||
val vibrator = getSystemService(Vibrator::class.java)
|
val vibrator = getSystemService(Vibrator::class.java)
|
||||||
if (vibrator != null && vibrator.hasVibrator()) {
|
if (vibrator != null && vibrator.hasVibrator()) {
|
||||||
|
ringVibrator = vibrator
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val pattern = longArrayOf(0, 400, 200, 400, 200, 400)
|
val pattern = longArrayOf(0, 400, 200, 400, 200, 400)
|
||||||
vibrator.vibrate(
|
vibrator.vibrate(
|
||||||
@ -360,6 +453,41 @@ class WearBaresipService : Service() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w("Baresip Wear", "vibrate failed: ${e.message}")
|
Log.w("Baresip Wear", "vibrate failed: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watchdog: if the call is still "incoming call" after 30s (remote
|
||||||
|
// never answered and no call-closed event arrived), stop the ring and
|
||||||
|
// clear the heads-up so the watch doesn't buzz forever.
|
||||||
|
try {
|
||||||
|
val cp = CallState.incoming()?.callp ?: 0L
|
||||||
|
watchdogHandler.postDelayed({
|
||||||
|
val stillIncoming = CallState.incoming() != null &&
|
||||||
|
CallState.incoming()?.callp == cp
|
||||||
|
if (stillIncoming) {
|
||||||
|
Log.w("Baresip Wear", "ring watchdog: no answer/closed in 30s, stopping")
|
||||||
|
stopRinging()
|
||||||
|
dismissIncomingCallNotification()
|
||||||
|
CallState.calls.toList().forEach {
|
||||||
|
if (it.callp == cp) CallState.remove(cp)
|
||||||
|
}
|
||||||
|
CallState.status.value = "Idle"
|
||||||
|
}
|
||||||
|
}, 30_000L)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("Baresip Wear", "ring watchdog failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stop the ring vibration (called on answer / decline / remote hangup). */
|
||||||
|
private fun stopRinging() {
|
||||||
|
try {
|
||||||
|
ringVibrator?.cancel()
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
// Belt-and-braces: also try the system vibrator directly in case the
|
||||||
|
// stored reference was lost.
|
||||||
|
try {
|
||||||
|
val v = getSystemService(Vibrator::class.java)
|
||||||
|
v?.cancel()
|
||||||
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove the incoming-call heads-up once the call is answered/declined. */
|
/** Remove the incoming-call heads-up once the call is answered/declined. */
|
||||||
@ -379,6 +507,10 @@ class WearBaresipService : Service() {
|
|||||||
const val CALL_CHANNEL_ID = "baresip-wear-call"
|
const val CALL_CHANNEL_ID = "baresip-wear-call"
|
||||||
const val CALL_NOTIFICATION_ID = 2
|
const val CALL_NOTIFICATION_ID = 2
|
||||||
|
|
||||||
|
// Tracks the live call's native handle for hangup/decline. Updated on
|
||||||
|
// incoming/established/closed events. A single call at a time on the watch.
|
||||||
|
@JvmStatic var activeCallp: Long = 0L
|
||||||
|
|
||||||
// Application context, captured in onCreate, for toasts from non-UI code.
|
// Application context, captured in onCreate, for toasts from non-UI code.
|
||||||
lateinit var appContext: Context
|
lateinit var appContext: Context
|
||||||
private set
|
private set
|
||||||
|
|||||||
@ -58,14 +58,38 @@ object WearBaresipServiceHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun answer(call: WearCall) {
|
fun answer(call: WearCall) {
|
||||||
if (call.uap != 0L) Api.ua_answer(call.uap, call.callp, Api.VIDMODE_OFF)
|
// Answer the inbound call: send the 200 OK via ua_answer on the real
|
||||||
else if (defaultUap != 0L) Api.ua_answer(defaultUap, call.callp, Api.VIDMODE_OFF)
|
// struct call* captured at the "call incoming" event. Api.ua_answer
|
||||||
|
// defers the native call to the baresip main loop (tmr), so invoking
|
||||||
|
// it from the UI thread is safe (a direct UI-thread call crashes in
|
||||||
|
// sip_treplyf). The session was already "accepted" (provisional 100/180
|
||||||
|
// so the caller hears ringing) at the "incoming call" event.
|
||||||
|
val uap = if (call.uap != 0L) call.uap else defaultUap
|
||||||
|
val liveCallp = WearBaresipService.activeCallp
|
||||||
|
val target = if (liveCallp != 0L) liveCallp else call.callp
|
||||||
|
if (target == 0L || uap == 0L) {
|
||||||
|
android.util.Log.w("Baresip Wear", "answer: invalid handles uap=$uap target=$target")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
android.util.Log.d("Baresip Wear", "ua_answer uap=$uap callp=$target (deferred to main loop)")
|
||||||
|
Api.ua_answer(uap, target, Api.VIDMODE_OFF)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decline/reject the ringing call, or hang up an active one. Uses the
|
||||||
|
// service's tracked activeCallp (updated on established/closed events),
|
||||||
|
// because the event-time SIP message pointer is not a valid call* once the
|
||||||
|
// call is allocated.
|
||||||
fun hangup(call: WearCall) {
|
fun hangup(call: WearCall) {
|
||||||
val uap = if (call.uap != 0L) call.uap else defaultUap
|
val uap = if (call.uap != 0L) call.uap else defaultUap
|
||||||
Api.ua_hangup(uap, call.callp, 0, "")
|
val liveCallp = WearBaresipService.activeCallp
|
||||||
CallState.remove(call.callp)
|
val target = if (liveCallp != 0L) liveCallp else call.callp
|
||||||
|
if (target == 0L || uap == 0L) {
|
||||||
|
android.util.Log.w("Baresip Wear", "hangup: invalid handles uap=$uap target=$target")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
android.util.Log.d("Baresip Wear", "ua_hangup uap=$uap callp=$target")
|
||||||
|
Api.ua_hangup(uap, target, 0, "")
|
||||||
|
CallState.remove(target)
|
||||||
if (CallState.calls.isEmpty()) CallState.status.value = "Idle"
|
if (CallState.calls.isEmpty()) CallState.status.value = "Idle"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user