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") + } +}