diff --git a/.agentbridge/conversation.db b/.agentbridge/conversation.db
index 4d0f4d75..f5c01f75 100644
Binary files a/.agentbridge/conversation.db and b/.agentbridge/conversation.db differ
diff --git a/.agentbridge/conversation.db-shm b/.agentbridge/conversation.db-shm
deleted file mode 100644
index 63921544..00000000
Binary files a/.agentbridge/conversation.db-shm and /dev/null differ
diff --git a/.agentbridge/conversation.db-wal b/.agentbridge/conversation.db-wal
deleted file mode 100644
index b205380c..00000000
Binary files a/.agentbridge/conversation.db-wal and /dev/null differ
diff --git a/.gitignore b/.gitignore
index 61d810f9..65e720b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,6 +18,8 @@ build
.externalNativeBuild
/app/release
/app/.cxx
+/wear/.cxx
+/wear/build
gradle/wrapper/gradle-wrapper.jar
/distribution/*
/distribution.video/*
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 29271b81..80ab3721 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,6 +8,7 @@ fragmentKtx = "1.8.9"
gradleVersion = "9.3.1"
kotlin = "2.4.10"
kotlinStdlibJdk8 = "2.4.0"
+android = "2.4.10"
kotlinSerializationPlugin = "2.4.10"
kotlinxCoroutinesAndroid = "1.11.0"
kotlinxSerializationJson = "1.11.0"
@@ -25,6 +26,8 @@ navigationRuntimeAndroid = "2.9.8"
composeMaterialIcons = "1.7.8"
runtime = "1.11.4"
uiText = "1.11.4"
+wear = "1.4.0"
+wearCompose = "1.4.1"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
@@ -54,7 +57,12 @@ androidx-compose-material-icons-core = { group = "androidx.compose.material", na
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "composeMaterialIcons" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
androidx-compose-ui-text = { group = "androidx.compose.ui", name = "ui-text", version.ref = "uiText" }
+androidx-wear = { module = "androidx.wear:wear", version.ref = "wear" }
+androidx-wear-compose = { group = "androidx.wear.compose", name = "compose-material", version.ref = "wearCompose" }
+androidx-wear-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "wearCompose" }
+androidx-wear-compose-navigation = { group = "androidx.wear.compose", name = "compose-navigation", version.ref = "wearCompose" }
[plugins]
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinSerializationPlugin" }
+kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "android" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 21dd416b..b6f8cf58 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,5 +1,2 @@
-plugins {
- id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
-}
-
-include (":app")
+include(":app")
+include(":wear")
diff --git a/wear/build.gradle.kts b/wear/build.gradle.kts
new file mode 100644
index 00000000..98189e8a
--- /dev/null
+++ b/wear/build.gradle.kts
@@ -0,0 +1,52 @@
+plugins {
+ id("com.android.application")
+ alias(libs.plugins.compose.compiler)
+}
+
+android {
+ namespace = "com.tutpro.baresip.wear"
+ compileSdk = 37
+ ndkVersion = "29.0.14206865"
+
+ defaultConfig {
+ applicationId = "com.tutpro.baresip.wear"
+ minSdk = 30
+ targetSdk = 36
+ versionCode = 1
+ versionName = "0.1.0-wear"
+
+ externalNativeBuild {
+ cmake {
+ cFlags += "-DHAVE_INTTYPES_H -lstdc++"
+ arguments.addAll(listOf("-DANDROID_STL=c++_shared"))
+ }
+ }
+ ndk {
+ abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a"))
+ }
+ }
+
+ buildFeatures {
+ compose = true
+ buildConfig = true
+ }
+
+ externalNativeBuild {
+ cmake {
+ path = file("src/main/cpp/CMakeLists.txt")
+ version = "3.31.6"
+ }
+ }
+}
+
+dependencies {
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.androidx.core.ktx)
+ implementation(libs.androidx.lifecycle.runtime.compose)
+ implementation(libs.androidx.lifecycle.viewmodel.ktx)
+ implementation(libs.androidx.activity.compose)
+ implementation(libs.androidx.wear)
+ implementation(libs.androidx.wear.compose)
+ implementation(libs.androidx.wear.compose.foundation)
+ implementation(libs.androidx.wear.compose.navigation)
+}
diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..8093af0d
--- /dev/null
+++ b/wear/src/main/AndroidManifest.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wear/src/main/cpp/CMakeLists.txt b/wear/src/main/cpp/CMakeLists.txt
new file mode 100644
index 00000000..4c04408e
--- /dev/null
+++ b/wear/src/main/cpp/CMakeLists.txt
@@ -0,0 +1,98 @@
+cmake_minimum_required(VERSION 3.18...4.0)
+
+project(baresip-wear)
+
+add_link_options("LINKER:--build-id=none")
+
+set(distribution_DIR ${CMAKE_SOURCE_DIR}/../../../../distribution)
+
+add_library(lib_crypto STATIC IMPORTED)
+set_target_properties(lib_crypto PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/openssl/lib/${ANDROID_ABI}/libcrypto.a)
+
+add_library(lib_ssl STATIC IMPORTED)
+set_target_properties(lib_ssl PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/openssl/lib/${ANDROID_ABI}/libssl.a)
+
+add_library(lib_re STATIC IMPORTED)
+set_target_properties(lib_re PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/re/lib/${ANDROID_ABI}/libre.a)
+
+add_library(lib_opus STATIC IMPORTED)
+set_target_properties(lib_opus PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/opus/lib/${ANDROID_ABI}/libopus.a)
+
+add_library(lib_g722 STATIC IMPORTED)
+set_target_properties(lib_g722 PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/g722/lib/${ANDROID_ABI}/libg722.a)
+
+add_library(lib_g722_1 STATIC IMPORTED)
+set_target_properties(lib_g722_1 PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/g7221/lib/${ANDROID_ABI}/libg722_1.a)
+
+add_library(lib_g729 STATIC IMPORTED)
+set_target_properties(lib_g729 PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/g729/lib/${ANDROID_ABI}/libbcg729.a)
+
+add_library(lib_ilbc STATIC IMPORTED)
+set_target_properties(lib_ilbc PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/ilbc/lib/${ANDROID_ABI}/libilbc.a)
+
+add_library(lib_codec2 STATIC IMPORTED)
+set_target_properties(lib_codec2 PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/codec2/lib/${ANDROID_ABI}/libcodec2.a)
+
+add_library(lib_amrnb STATIC IMPORTED)
+set_target_properties(lib_amrnb PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/amr/lib/${ANDROID_ABI}/libamrnb.a)
+
+add_library(lib_amrwb STATIC IMPORTED)
+set_target_properties(lib_amrwb PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/amr/lib/${ANDROID_ABI}/libamrwb.a)
+
+add_library(lib_amrwbenc STATIC IMPORTED)
+set_target_properties(lib_amrwbenc PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/amr/lib/${ANDROID_ABI}/libamrwbenc.a)
+
+add_library(lib_zrtpcppcore STATIC IMPORTED)
+set_target_properties(lib_zrtpcppcore PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/gzrtp/lib/${ANDROID_ABI}/libzrtpcppcore.a)
+
+add_library(lib_sndfile STATIC IMPORTED)
+set_target_properties(lib_sndfile PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/sndfile/lib/${ANDROID_ABI}/libsndfile.a)
+
+add_library(lib_baresip STATIC IMPORTED)
+set_target_properties(lib_baresip PROPERTIES IMPORTED_LOCATION
+ ${distribution_DIR}/baresip/lib/${ANDROID_ABI}/libbaresip.a)
+
+add_library(wearbaresip SHARED ${CMAKE_SOURCE_DIR}/baresip.c)
+
+target_include_directories(wearbaresip PRIVATE
+ ${distribution_DIR}/openssl/include
+ ${distribution_DIR}/re/include
+ ${distribution_DIR}/baresip/include)
+
+add_definitions(-DHAVE_PTHREAD)
+
+target_link_libraries(
+ wearbaresip
+ android
+ aaudio
+ lib_baresip
+ lib_re
+ lib_ssl
+ lib_crypto
+ lib_opus
+ lib_g722
+ lib_g722_1
+ lib_g729
+ lib_ilbc
+ lib_codec2
+ lib_amrnb
+ lib_amrwb
+ lib_amrwbenc
+ lib_zrtpcppcore
+ lib_sndfile
+ z
+ log)
diff --git a/wear/src/main/cpp/baresip.c b/wear/src/main/cpp/baresip.c
new file mode 100644
index 00000000..659e36e9
--- /dev/null
+++ b/wear/src/main/cpp/baresip.c
@@ -0,0 +1,466 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "logger.h"
+
+typedef struct baresip_context
+{
+ JavaVM *javaVM;
+ jclass serviceClz;
+ jobject serviceObj;
+
+} BaresipContext;
+
+static BaresipContext g_ctx;
+
+enum
+{
+ ASYNC_WORKERS = 4
+};
+
+static pthread_key_t g_thread_key;
+
+static void detach_thread(void *env)
+{
+ (void)env;
+ (*g_ctx.javaVM)->DetachCurrentThread(g_ctx.javaVM);
+}
+
+static JNIEnv *get_jni_env(void)
+{
+ JNIEnv *env;
+ jint res = (*g_ctx.javaVM)->GetEnv(g_ctx.javaVM, (void**)&env, JNI_VERSION_1_6);
+ if (res != JNI_OK) {
+ res = (*g_ctx.javaVM)->AttachCurrentThread(g_ctx.javaVM, &env, NULL);
+ if (res == JNI_OK) {
+ pthread_setspecific(g_thread_key, env);
+ } else {
+ return NULL;
+ }
+ }
+ return env;
+}
+
+static void ua_exit_handler(void *arg)
+{
+ (void)arg;
+ re_cancel();
+}
+
+static const char *translate_errorcode(uint16_t scode)
+{
+ switch (scode) {
+ case 404:
+ return "";
+ case 486:
+ case 603:
+ return "busy";
+ case 487:
+ return "";
+ default:
+ return "error";
+ }
+}
+
+static void event_handler(enum bevent_ev ev, struct bevent *event, void *arg)
+{
+ (void)arg;
+ const char *prm = bevent_get_text(event);
+ struct call *call = bevent_get_call(event);
+ struct ua *ua = bevent_get_ua(event);
+ const struct sip_msg *msg = bevent_get_msg(event);
+ struct account *acc = ua_account(bevent_get_ua(event));
+ const char *tone;
+ char event_buf[256];
+ enum sdp_dir ardir;
+ int len, err;
+ struct pl module, module_event, data;
+
+ switch (ev) {
+ case BEVENT_CREATE:
+ len = re_snprintf(event_buf, sizeof event_buf, "create", "");
+ break;
+ case BEVENT_REGISTERING:
+ case BEVENT_UNREGISTERING:
+ case BEVENT_REGISTER_OK:
+ case BEVENT_FALLBACK_OK:
+ len = re_snprintf(event_buf, sizeof event_buf, "%s", prm);
+ break;
+ case BEVENT_REGISTER_FAIL:
+ case BEVENT_FALLBACK_FAIL:
+ len = re_snprintf(event_buf, sizeof event_buf, "registering failed,%s", prm);
+ break;
+ case BEVENT_SIPSESS_CONN:
+ ua = uag_find_msg(msg);
+ call = (struct call *)msg;
+ len = re_snprintf(event_buf, sizeof event_buf, "%s,%r,%ld", prm, &msg->from.auri, (long)event);
+ break;
+ case BEVENT_CALL_INCOMING:
+ len = re_snprintf(event_buf, sizeof event_buf, "call incoming,%s", prm);
+ break;
+ case BEVENT_CALL_OUTGOING:
+ len = re_snprintf(event_buf, sizeof event_buf, "call outgoing", "");
+ break;
+ case BEVENT_CALL_ANSWERED:
+ len = re_snprintf(event_buf, sizeof event_buf, "call answered", "");
+ break;
+ case BEVENT_CALL_REDIRECT:
+ len = re_snprintf(event_buf, sizeof event_buf, "call redirect,%s", prm + 4);
+ break;
+ case BEVENT_CALL_LOCAL_SDP:
+ if (strcmp(prm, "offer") == 0)
+ return;
+ len = re_snprintf(event_buf, sizeof event_buf, "call %sed", prm);
+ break;
+ case BEVENT_CALL_RINGING:
+ len = re_snprintf(event_buf, sizeof event_buf, "call ringing", "");
+ break;
+ case BEVENT_CALL_PROGRESS:
+ ardir = sdp_media_rdir(stream_sdpmedia(audio_strm(call_audio(call))));
+ len = re_snprintf(event_buf, sizeof event_buf, "call progress,%d", ardir);
+ break;
+ case BEVENT_CALL_ESTABLISHED:
+ len = re_snprintf(event_buf, sizeof event_buf, "call established", "");
+ break;
+ case BEVENT_CALL_REMOTE_SDP:
+ ardir = sdp_media_rdir(stream_sdpmedia(audio_strm(call_audio(call))));
+ len = re_snprintf(event_buf, sizeof event_buf, "call update,%d,%ld", ardir, (long)event);
+ break;
+ case BEVENT_CALL_MENC:
+ if (prm[0] == '0')
+ len = re_snprintf(event_buf, sizeof event_buf, "call secure", "");
+ else if (prm[0] == '1')
+ len = re_snprintf(event_buf, sizeof event_buf, "call verify,%s", prm + 2);
+ else if (prm[0] == '2')
+ len = re_snprintf(event_buf, sizeof event_buf, "call verified,%s", prm + 2);
+ else
+ len = re_snprintf(event_buf, sizeof event_buf, "unknown menc event", "");
+ break;
+ case BEVENT_CALL_TRANSFER:
+ len = re_snprintf(event_buf, sizeof event_buf, "call transfer,%s", prm);
+ break;
+ case BEVENT_CALL_TRANSFER_FAILED:
+ call_hold(call, false);
+ len = re_snprintf(event_buf, sizeof event_buf, "transfer failed,%s", prm);
+ break;
+ case BEVENT_CALL_CLOSED:
+ tone = call_scode(call) ? translate_errorcode(call_scode(call)) : "";
+ len = re_snprintf(event_buf, sizeof event_buf, "call closed,%s,%s", prm, tone);
+ break;
+ case BEVENT_MWI_NOTIFY:
+ len = re_snprintf(event_buf, sizeof event_buf, "mwi notify,%s", prm);
+ break;
+ case BEVENT_MODULE:
+ err = re_regex(prm, strlen(prm), "[^,]*,[^,]*,[~]*", &module, &module_event, &data);
+ if (err)
+ return;
+ if (!pl_strcmp(&module_event, "dump")) {
+ len = re_snprintf(event_buf, sizeof event_buf, "sndfile dump,%r", &data);
+ break;
+ }
+ if (!pl_strcmp(&module_event, "recorder sessionid")) {
+ len = re_snprintf(event_buf, sizeof event_buf, "recorder sessionid,%r", &data);
+ break;
+ }
+ default:
+ return;
+ }
+
+ if (len == -1) {
+ return;
+ }
+
+ JNIEnv *env = get_jni_env();
+ if (!env) return;
+
+ if (!g_ctx.serviceClz || !g_ctx.serviceObj) return;
+
+ jmethodID methodId =
+ (*env)->GetMethodID(env, g_ctx.serviceClz, "uaEvent", "(Ljava/lang/String;)V");
+ jstring jEvent = (*env)->NewStringUTF(env, event_buf);
+ if (methodId) {
+ (*env)->CallVoidMethod(env, g_ctx.serviceObj, methodId, jEvent);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ }
+ }
+ (*env)->DeleteLocalRef(env, jEvent);
+}
+
+static void message_handler(
+ struct ua *ua, const struct pl *peer, const struct pl *ctype, struct mbuf *body, void *arg)
+{
+ (void)arg;
+ char peer_buf[256];
+ size_t size;
+
+ if (snprintf(peer_buf, 256, "%.*s", (int)peer->l, peer->p) >= 256) {
+ return;
+ }
+
+ JNIEnv *env = get_jni_env();
+ if (!env) return;
+
+ if (!g_ctx.serviceClz || !g_ctx.serviceObj) return;
+
+ jmethodID methodId = (*env)->GetMethodID(env, g_ctx.serviceClz, "messageEvent",
+ "(JLjava/lang/String;Ljava/lang/String;[B)V");
+ jstring jPeer = (*env)->NewStringUTF(env, peer_buf);
+ jbyteArray jMsg;
+ size = mbuf_get_left(body);
+ jMsg = (*env)->NewByteArray(env, (jsize)size);
+ if ((*env)->GetArrayLength(env, jMsg) != size) {
+ (*env)->DeleteLocalRef(env, jMsg);
+ jMsg = (*env)->NewByteArray(env, (jsize)size);
+ }
+ void *temp = (*env)->GetPrimitiveArrayCritical(env, (jarray)jMsg, 0);
+ memcpy(temp, mbuf_buf(body), size);
+ (*env)->ReleasePrimitiveArrayCritical(env, jMsg, temp, 0);
+ if (methodId) {
+ (*env)->CallVoidMethod(env, g_ctx.serviceObj, methodId, (jlong)ua, jPeer, NULL, jMsg);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ }
+ }
+ (*env)->DeleteLocalRef(env, jPeer);
+ (*env)->DeleteLocalRef(env, jMsg);
+}
+
+static void send_resp_handler(int err, const struct sip_msg *msg, void *arg)
+{
+ char *native_time = (char *)arg;
+ char reason_buf[64];
+
+ if (err) {
+ mem_deref(native_time);
+ return;
+ }
+
+ pl_strcpy(&(msg->reason), reason_buf, 64);
+
+ JNIEnv *env = get_jni_env();
+ if (!env) {
+ mem_deref(native_time);
+ return;
+ }
+
+ if (!g_ctx.serviceClz || !g_ctx.serviceObj) {
+ mem_deref(native_time);
+ return;
+ }
+
+ jmethodID methodId = (*env)->GetMethodID(env, g_ctx.serviceClz, "messageResponse",
+ "(ILjava/lang/String;Ljava/lang/String;)V");
+ jstring javaReason = (*env)->NewStringUTF(env, reason_buf);
+ jstring javaTime = (*env)->NewStringUTF(env, native_time);
+ if (methodId) {
+ (*env)->CallVoidMethod(env, g_ctx.serviceObj, methodId, msg->scode, javaReason,
+ javaTime);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ }
+ }
+ (*env)->DeleteLocalRef(env, javaReason);
+ (*env)->DeleteLocalRef(env, javaTime);
+
+ mem_deref(native_time);
+}
+
+enum
+{
+ ID_UA_STOP_ALL
+};
+
+static struct mqueue *mq;
+
+static void mqueue_handler(int id, void *data, void *arg)
+{
+ (void)arg;
+ if (id == ID_UA_STOP_ALL) {
+ ua_stop_all((bool)(uintptr_t)data);
+ }
+}
+
+#include
+
+static int pfd[2];
+static pthread_t loggingThread;
+
+static void *loggingFunction(void *arg)
+{
+ (void)arg;
+ ssize_t readSize;
+ char buf[128];
+
+ while ((readSize = read(pfd[0], buf, sizeof buf - 1)) > 0) {
+ if (buf[readSize - 1] == '\n') {
+ --readSize;
+ }
+ buf[readSize] = 0;
+ LOGD("%s", buf);
+ }
+
+ return 0;
+}
+
+static int runLoggingThread()
+{
+ setvbuf(stdout, 0, _IOLBF, 0);
+ setvbuf(stderr, 0, _IONBF, 0);
+
+ pipe(pfd);
+ dup2(pfd[1], 1);
+ dup2(pfd[1], 2);
+
+ int ret = pthread_create(&loggingThread, NULL, loggingFunction, NULL);
+ if (ret != 0) {
+ return ret;
+ }
+
+ pthread_detach(loggingThread);
+
+ return 0;
+}
+
+JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
+{
+ (void)reserved;
+
+ memset(&g_ctx, 0, sizeof(g_ctx));
+ g_ctx.javaVM = vm;
+
+ if (pthread_key_create(&g_thread_key, detach_thread) != 0) {
+ }
+
+ return JNI_VERSION_1_6;
+}
+
+JNIEXPORT void JNICALL
+Java_com_tutpro_baresip_wear_WearBaresipService_baresipStart(
+ JNIEnv *env, jobject instance, jstring jPath, jstring jAddrs, jint jLogLevel, jstring jSoftware)
+{
+ int err;
+
+ jclass clz = (*env)->GetObjectClass(env, instance);
+ g_ctx.serviceClz = (*env)->NewGlobalRef(env, clz);
+ g_ctx.serviceObj = (*env)->NewGlobalRef(env, instance);
+
+ const char *path = (*env)->GetStringUTFChars(env, jPath, 0);
+ const char *addrs = (*env)->GetStringUTFChars(env, jAddrs, 0);
+ const char *software = (*env)->GetStringUTFChars(env, jSoftware, 0);
+
+ runLoggingThread();
+
+ err = libre_init();
+ if (err) {
+ goto out;
+ }
+
+ if (re_thread_check(true) == 0) {
+ jint res = (*g_ctx.javaVM)->AttachCurrentThread(g_ctx.javaVM, &env, NULL);
+ if (JNI_OK != res) {
+ goto out;
+ }
+ } else {
+ goto out;
+ }
+
+ conf_path_set(path);
+
+ log_level_set((enum log_level)jLogLevel);
+
+ err = conf_configure();
+ if (err) {
+ goto out;
+ }
+
+ re_thread_async_init(ASYNC_WORKERS);
+
+ err = baresip_init(conf_config());
+ if (err) {
+ goto out;
+ }
+
+ dnsc_cache_max(net_dnsc(baresip_network()), 0);
+
+ if (strlen(addrs) > 0) {
+ char *addr_list = (char *)malloc(strlen(addrs) + 1);
+ struct sa temp_sa;
+ char buf[256];
+ net_flush_addresses(baresip_network());
+ strcpy(addr_list, addrs);
+ char *ptr = strtok(addr_list, ";");
+ while (ptr != NULL) {
+ if (0 == sa_set_str(&temp_sa, ptr, 0)) {
+ sa_ntop(&temp_sa, buf, 256);
+ ptr = strtok(NULL, ";");
+ net_add_address_ifname(baresip_network(), &temp_sa, ptr);
+ } else {
+ ptr = strtok(NULL, ";");
+ }
+ if (ptr)
+ *(ptr - 1) = ';';
+ ptr = strtok(NULL, ";");
+ }
+ free(addr_list);
+ }
+
+ err = ua_init(software, true, true, true);
+ if (err) {
+ goto out;
+ }
+
+ uag_set_exit_handler(ua_exit_handler, NULL);
+
+ err = bevent_register(event_handler, NULL);
+ if (err) {
+ goto out;
+ }
+
+ err = message_listen(baresip_message(), message_handler, NULL);
+ if (err) {
+ goto out;
+ }
+
+ err = conf_modules();
+ if (err) {
+ goto out;
+ }
+
+ err = mqueue_alloc(&mq, mqueue_handler, NULL);
+ if (err) {
+ goto out;
+ }
+
+out:
+ (*env)->ReleaseStringUTFChars(env, jPath, path);
+ (*env)->ReleaseStringUTFChars(env, jAddrs, addrs);
+ (*env)->ReleaseStringUTFChars(env, jSoftware, software);
+}
+
+JNIEXPORT void JNICALL
+Java_com_tutpro_baresip_wear_WearBaresipService_baresipStop(JNIEnv *env, jobject instance)
+{
+ (void)env;
+ (void)instance;
+
+ ua_stop_all(true);
+ baresip_close();
+ libre_close();
+
+ if (g_ctx.serviceClz) {
+ (*env)->DeleteGlobalRef(env, g_ctx.serviceClz);
+ g_ctx.serviceClz = NULL;
+ }
+ if (g_ctx.serviceObj) {
+ (*env)->DeleteGlobalRef(env, g_ctx.serviceObj);
+ g_ctx.serviceObj = NULL;
+ }
+}
diff --git a/wear/src/main/cpp/logger.h b/wear/src/main/cpp/logger.h
new file mode 100644
index 00000000..7c9c1483
--- /dev/null
+++ b/wear/src/main/cpp/logger.h
@@ -0,0 +1,13 @@
+#include
+#include
+
+#ifndef BARESIP_LOGGER_H
+#define BARESIP_LOGGER_H
+
+#define LOG_TAG "Baresip Wear"
+#define LOGD(...) ((void)__android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__))
+#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))
+#define LOGW(...) ((void)__android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__))
+#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))
+
+#endif
diff --git a/wear/src/main/java/com/tutpro/baresip/wear/MainActivity.kt b/wear/src/main/java/com/tutpro/baresip/wear/MainActivity.kt
new file mode 100644
index 00000000..0fc9ffff
--- /dev/null
+++ b/wear/src/main/java/com/tutpro/baresip/wear/MainActivity.kt
@@ -0,0 +1,49 @@
+package com.tutpro.baresip.wear
+
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.TimeText
+
+class MainActivity : ComponentActivity() {
+
+ private val recordAudioPermission = registerForActivityResult(
+ ActivityResultContracts.RequestPermission()
+ ) { granted ->
+ if (granted) {
+ startBaresipService()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ TimeText()
+ Text(text = "Baresip Wear")
+ }
+
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
+ == PackageManager.PERMISSION_GRANTED
+ ) {
+ startBaresipService()
+ } else {
+ recordAudioPermission.launch(Manifest.permission.RECORD_AUDIO)
+ }
+ }
+
+ private fun startBaresipService() {
+ val intent = Intent(this, WearBaresipService::class.java)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(intent)
+ } else {
+ startService(intent)
+ }
+ }
+}
diff --git a/wear/src/main/java/com/tutpro/baresip/wear/WearBaresipApp.kt b/wear/src/main/java/com/tutpro/baresip/wear/WearBaresipApp.kt
new file mode 100644
index 00000000..29bdaef4
--- /dev/null
+++ b/wear/src/main/java/com/tutpro/baresip/wear/WearBaresipApp.kt
@@ -0,0 +1,10 @@
+package com.tutpro.baresip.wear
+
+import android.app.Application
+
+class WearBaresipApp : Application() {
+ // The foreground service is launched from MainActivity (foreground state),
+ // not from Application.onCreate(), because starting a microphone-type
+ // foreground service from a non-foreground context is rejected on
+ // targetSdk 34+.
+}
diff --git a/wear/src/main/java/com/tutpro/baresip/wear/WearBaresipService.kt b/wear/src/main/java/com/tutpro/baresip/wear/WearBaresipService.kt
new file mode 100644
index 00000000..cf208f43
--- /dev/null
+++ b/wear/src/main/java/com/tutpro/baresip/wear/WearBaresipService.kt
@@ -0,0 +1,65 @@
+package com.tutpro.baresip.wear
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.os.Build
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+
+class WearBaresipService : Service() {
+
+ override fun onCreate() {
+ super.onCreate()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "Baresip Wear",
+ NotificationManager.IMPORTANCE_LOW
+ )
+ val manager = getSystemService(NotificationManager::class.java)
+ manager.createNotificationChannel(channel)
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("Baresip Wear")
+ .setContentText("SIP stack running")
+ .setSmallIcon(android.R.drawable.sym_call_incoming)
+ .build()
+ startForeground(NOTIFICATION_ID, notification)
+
+ // Start the native baresip stack. Path points at the app's private
+ // config directory where accounts.cfg etc. are expected.
+ val path = applicationContext.filesDir.absolutePath
+ baresipStart(path, "", 2, "baresip-studio-wear")
+
+ return START_STICKY
+ }
+
+ override fun onDestroy() {
+ baresipStop()
+ super.onDestroy()
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ external fun baresipStart(path: String, addrs: String, logLevel: Int, software: String)
+ external fun baresipStop()
+
+ fun uaEvent(event: String) {
+ // TODO route to watch UI / Data Layer
+ }
+
+ companion object {
+ const val CHANNEL_ID = "baresip-wear"
+ const val NOTIFICATION_ID = 1
+ }
+
+ init {
+ System.loadLibrary("wearbaresip")
+ }
+}