/* * android_jni_bridge.c — JNI bridge implementation * * C side of the bridge. Wires Kotlin ↔ uTun stack: * - nativeStart(configText) → instance_lite_start() * - nativeJoinChannel(chId, nodeId, pubkey, addrs) → chat_sync_connect_from_invite() * - Chat events → JNI callback → Kotlin */ #include "android_jni_bridge.h" #include "android_udp_log.h" #include "../libutun_lite/utun_config_api.h" #include "../libutun_lite/invite_link_c.h" #include "../libutun_lite/instance_lite.h" #include "../../../lib/debug_config.h" #include "../../../lib/u_async.h" #include "../../../lib/mem.h" #include "../../../lib/sqlite3.h" #include #include #include #include #include #ifdef __ANDROID__ #include #include #define LOG_TAG "utun-bridge" #else #define ANDROID_LOG_VERBOSE 2 #define ANDROID_LOG_DEBUG 3 #define ANDROID_LOG_INFO 4 #define ANDROID_LOG_WARN 5 #define ANDROID_LOG_ERROR 6 #endif enum { BLEV_TRACE = 0, BLEV_DEBUG = 1, BLEV_INFO = 2, BLEV_WARN = 3, BLEV_ERROR = 4 }; static int blevel_to_android(int blevel) { switch (blevel) { case BLEV_TRACE: return ANDROID_LOG_VERBOSE; case BLEV_DEBUG: return ANDROID_LOG_DEBUG; case BLEV_INFO: return ANDROID_LOG_INFO; case BLEV_WARN: return ANDROID_LOG_WARN; case BLEV_ERROR: return ANDROID_LOG_ERROR; default: return ANDROID_LOG_VERBOSE; } } static utun_log_fn g_on_log = NULL; static utun_event_fn g_on_event = NULL; static void bridge_log(int level, const char* fmt, ...) { va_list ap; va_start(ap, fmt); #ifdef __ANDROID__ __android_log_vprint(blevel_to_android(level), LOG_TAG, fmt, ap); #else vfprintf(stderr, fmt, ap); fputc('\n', stderr); #endif va_end(ap); if (g_on_log) { va_list ap2; char buf[512]; va_start(ap2, fmt); vsnprintf(buf, sizeof(buf), fmt, ap2); va_end(ap2); const char* cat; switch (level) { case BLEV_TRACE: cat = "TRACE"; break; case BLEV_DEBUG: cat = "DEBUG"; break; case BLEV_INFO: cat = "INFO"; break; case BLEV_WARN: cat = "WARN"; break; case BLEV_ERROR: cat = "ERROR"; break; default: cat = "?"; break; } g_on_log(level, cat, buf); } } static void bridge_debug_hook(int level, const char* category, const char* message) { #ifdef __ANDROID__ __android_log_print(blevel_to_android(level), LOG_TAG, "[%s] %s", category ? category : "?", message ? message : ""); #else fprintf(stderr, "[%s] %s\n", category ? category : "?", message ? message : ""); #endif udp_log_write(message, (int)strlen(message)); if (g_on_log) g_on_log(level, category, message); } static utun_config_string_fn g_cfg_string = NULL; static utun_config_int64_fn g_cfg_int64 = NULL; static utun_config_int_fn g_cfg_int = NULL; static const char* cfg_wrapper_string(const char* key, const char* def) { if (g_cfg_string) { const char* val = g_cfg_string(key); return val ? val : def; } return def; } static int64_t cfg_wrapper_int64(const char* key, int64_t def) { if (g_cfg_int64) return g_cfg_int64(key); return def; } static int cfg_wrapper_int(const char* key, int def) { if (g_cfg_int) return g_cfg_int(key); return def; } /* ── Public API ── */ void utun_bridge_log(int level, const char* category, const char* message) { if (g_on_log) g_on_log(level, category, message); } void utun_bridge_event(int event_type, const char* json_data) { if (g_on_event) g_on_event(event_type, json_data); } bool utun_bridge_init(const char* db_path, int control_port, utun_log_fn on_log, utun_event_fn on_event, const struct utun_bridge_config* config) { g_on_log = on_log; g_on_event = on_event; debug_set_log_hook(bridge_debug_hook); if (config) { g_cfg_string = config->get_string; g_cfg_int64 = config->get_int64; g_cfg_int = config->get_int; utun_config_provider_t provider = { .get_string = cfg_wrapper_string, .get_int64 = cfg_wrapper_int64, .get_int = cfg_wrapper_int, .get_bool = NULL, }; utun_config_set_provider(&provider); } bridge_log(BLEV_INFO, "bridge init: db=%s port=%d (stub, use nativeStart for real init)", db_path ? db_path : "(null)", control_port); return true; } void utun_bridge_destroy(void) { instance_lite_stop(); g_on_log = NULL; g_on_event = NULL; g_cfg_string = NULL; g_cfg_int64 = NULL; g_cfg_int = NULL; bridge_log(BLEV_INFO, "bridge destroy"); } void utun_bridge_start(const char* config_text) { bridge_log(BLEV_INFO, "bridge start: launching utun instance"); int rc = instance_lite_start(config_text); if (rc != 0) { bridge_log(BLEV_ERROR, "bridge start: instance_lite_start failed rc=%d", rc); } else { bridge_log(BLEV_INFO, "bridge start: instance running, node_id ready"); } } void utun_bridge_poll(void) { } void utun_bridge_send_message(const char* channel_id, const char* text) { bridge_log(BLEV_DEBUG, "send ch=%s text=%s", channel_id ? channel_id : "", text ? text : ""); } void utun_bridge_create_channel(const char* name, const char* channel_id) { bridge_log(BLEV_INFO, "create_channel name=%s id=%s", name ? name : "", channel_id ? channel_id : ""); } void utun_bridge_connect_node(const char* address, int port, const char* pubkey_hex) { bridge_log(BLEV_INFO, "connect %s:%d pubkey=%s", address ? address : "", port, pubkey_hex ? pubkey_hex : ""); } /* Real implementation: calls chat_sync_connect_from_invite() */ #include "chat/chat_sync.h" #include "chat/chat_core.h" void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id, const uint8_t* pubkey_bin, const uint8_t* addrs_data, int addr_count) { bridge_log(BLEV_INFO, "join ch=%llu node=0x%016llx addrs=%d", (unsigned long long)channel_id, (unsigned long long)node_id, addr_count); if (!pubkey_bin || !addrs_data || addr_count <= 0) { bridge_log(BLEV_ERROR, "join invalid args"); utun_bridge_event(6, "{\"node_id\":0,\"result\":-7}"); /* CHAT_EVT_CONNECT_RESULT */ return; } chat_sync_connect_from_invite(channel_id, node_id, pubkey_bin, addrs_data, addr_count); } void utun_bridge_connect_channel(const char* channel_id) { if (!channel_id || !channel_id[0]) { bridge_log(BLEV_ERROR, "connect_channel: null ch_id"); return; } struct UASYNC* ua = instance_lite_get_uasync(); if (!ua) { bridge_log(BLEV_ERROR, "connect_channel: ua not ready, ch=%s", channel_id); return; } char* cid = u_strdup(channel_id); if (!cid) return; bridge_log(BLEV_INFO, "connect_channel ch=%s", channel_id); uasync_post(ua, chat_core_connect_channel_trampoline, cid); } char* utun_bridge_get_channels_json(void) { return strdup("[]"); } char* utun_bridge_get_messages_json(const char* channel_id, int limit) { (void)limit; char buf[128]; snprintf(buf, sizeof(buf), "{\"channel\":\"%s\",\"messages\":[]}", channel_id ? channel_id : ""); return strdup(buf); } void utun_bridge_set_debug_level(const char* category, const char* level) { bridge_log(BLEV_INFO, "debug %s=%s", category ? category : "all", level ? level : "info"); } /* ────────────────────────────────────────────────────────────────── * JNI functions (compiled only for Android via NDK) * ────────────────────────────────────────────────────────────────── */ #ifdef __ANDROID__ static JavaVM* g_jvm = NULL; static jobject g_config_callback = NULL; static jobject g_log_callback = NULL; static jobject g_event_callback = NULL; static jmethodID g_event_mid = NULL; static char g_db_path[512]; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { (void)reserved; g_jvm = vm; return JNI_VERSION_1_6; } /* ── JNI log callback → LogManager.onNativeLog() ── */ static void jni_on_log(int level, const char* category, const char* message) { if (!g_jvm || !g_log_callback || !category || !message) return; JNIEnv* env = NULL; int attached = 0; jint res = (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6); if (res == JNI_EDETACHED) { if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != JNI_OK) return; attached = 1; } else if (res != JNI_OK) { return; } jclass cls = (*env)->GetObjectClass(env, g_log_callback); jmethodID mid = (*env)->GetMethodID(env, cls, "onNativeLog", "(ILjava/lang/String;Ljava/lang/String;)V"); if (!mid) { if (attached) (*g_jvm)->DetachCurrentThread(g_jvm); return; } jstring jcat = (*env)->NewStringUTF(env, category); jstring jmsg = (*env)->NewStringUTF(env, message); (*env)->CallVoidMethod(env, g_log_callback, mid, level, jcat, jmsg); (*env)->DeleteLocalRef(env, jmsg); (*env)->DeleteLocalRef(env, jcat); if (attached) (*g_jvm)->DetachCurrentThread(g_jvm); } /* ── JNI event callback (chat_event → Kotlin) ── */ static void jni_on_event(int type, const uint8_t* data, int len) { if (!g_jvm || !g_event_callback || !g_event_mid) return; JNIEnv* env = NULL; int attached = 0; jint res = (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6); if (res == JNI_EDETACHED) { if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != JNI_OK) return; attached = 1; } else if (res != JNI_OK) { return; } jbyteArray jdata = NULL; if (data && len > 0) { jdata = (*env)->NewByteArray(env, len); if (jdata) (*env)->SetByteArrayRegion(env, jdata, 0, len, (const jbyte*)data); } (*env)->CallVoidMethod(env, g_event_callback, g_event_mid, (jint)type, jdata); if (jdata) (*env)->DeleteLocalRef(env, jdata); if (attached) (*g_jvm)->DetachCurrentThread(g_jvm); } static void instance_event_handler(int type, const uint8_t* data, int len) { bridge_log(BLEV_DEBUG, "event type=%d len=%d", type, len); jni_on_event(type, data, len); } static jstring jni_get_string_callback(JNIEnv* env, const char* key) { if (!g_config_callback) return NULL; jclass cls = (*env)->GetObjectClass(env, g_config_callback); jmethodID mid = (*env)->GetMethodID(env, cls, "getString", "(Ljava/lang/String;)Ljava/lang/String;"); if (!mid) return NULL; jstring jkey = (*env)->NewStringUTF(env, key); jstring result = (jstring)(*env)->CallObjectMethod(env, g_config_callback, mid, jkey); (*env)->DeleteLocalRef(env, jkey); return result; } static const char* jni_cfg_get_string(const char* key) { JNIEnv* env = NULL; if (!g_jvm || (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) return NULL; jstring js = jni_get_string_callback(env, key); if (!js) return NULL; const char* result = (*env)->GetStringUTFChars(env, js, NULL); static char buf[256]; snprintf(buf, sizeof(buf), "%s", result ? result : ""); if (result) (*env)->ReleaseStringUTFChars(env, js, result); (*env)->DeleteLocalRef(env, js); return buf[0] ? buf : NULL; } static int64_t jni_cfg_get_int64(const char* key) { const char* s = jni_cfg_get_string(key); return s ? (int64_t)atoll(s) : 0; } static int jni_cfg_get_int(const char* key) { const char* s = jni_cfg_get_string(key); return s ? atoi(s) : 0; } /* ── nativeStart(configText) — real uTun instance launch ── */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeStart( JNIEnv* env, jobject thiz, jstring configText) { (void)thiz; if (!configText) { bridge_log(BLEV_ERROR, "nativeStart: null config"); return JNI_FALSE; } const char* cfg = (*env)->GetStringUTFChars(env, configText, NULL); if (!cfg) return JNI_FALSE; /* Set up event forwarder (bridge config/log are set by nativeInit) */ instance_lite_set_event_handler(instance_event_handler); utun_bridge_start(cfg); (*env)->ReleaseStringUTFChars(env, configText, cfg); return instance_lite_is_running() ? JNI_TRUE : JNI_FALSE; } /* ── nativeStop() ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeStop( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; utun_bridge_destroy(); udp_log_stop(); } /* ── nativeInit (legacy, just saves callbacks) ── */ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeInit( JNIEnv* env, jobject thiz, jstring dbPath, jint controlPort, jobject configCallback, jobject logCb) { (void)thiz; if (g_config_callback) (*env)->DeleteGlobalRef(env, g_config_callback); g_config_callback = configCallback ? (*env)->NewGlobalRef(env, configCallback) : NULL; if (g_log_callback) (*env)->DeleteGlobalRef(env, g_log_callback); g_log_callback = logCb ? (*env)->NewGlobalRef(env, logCb) : NULL; const char* db = dbPath ? (*env)->GetStringUTFChars(env, dbPath, NULL) : NULL; if (db) { strncpy(g_db_path, db, sizeof(g_db_path) - 1); g_db_path[sizeof(g_db_path) - 1] = '\0'; } /* Create empty DB file synchronously so ChatRepository (read-only) can open it immediately */ if (g_db_path[0]) { char db_file[512]; snprintf(db_file, sizeof(db_file), "%s/chats.db", g_db_path); sqlite3* js3; if (sqlite3_open_v2(db_file, &js3, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL) == SQLITE_OK) sqlite3_close(js3); } utun_log_fn log_cb = g_log_callback ? jni_on_log : NULL; struct utun_bridge_config cfg = { .get_string = jni_cfg_get_string, .get_int = jni_cfg_get_int, }; bool ok = utun_bridge_init(db, (int)controlPort, log_cb, NULL, &cfg); if (db) (*env)->ReleaseStringUTFChars(env, dbPath, db); return ok ? JNI_TRUE : JNI_FALSE; } /* ── nativeJoinChannel ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeJoinChannel( JNIEnv* env, jobject thiz, jlong channelId, jlong nodeId, jbyteArray pubkey, jbyteArray addrsData, jint addrCount) { (void)thiz; if (!pubkey || !addrsData) { bridge_log(BLEV_ERROR, "nativeJoinChannel null args"); return; } jsize pk_len = (*env)->GetArrayLength(env, pubkey); jsize ad_len = (*env)->GetArrayLength(env, addrsData); if (pk_len < 32 || ad_len <= 0) { bridge_log(BLEV_ERROR, "nativeJoinChannel bad sizes pk=%d ad=%d", (int)pk_len, (int)ad_len); return; } uint8_t* pk = (uint8_t*)(*env)->GetByteArrayElements(env, pubkey, NULL); uint8_t* ad = (uint8_t*)(*env)->GetByteArrayElements(env, addrsData, NULL); bridge_log(BLEV_DEBUG, "nativeJoinChannel ch=%lld node=0x%llx pk=%02x%02x... addrs_sz=%d", (long long)channelId, (unsigned long long)nodeId, pk[0], pk[1], (int)ad_len); utun_bridge_join_channel((uint64_t)channelId, (uint64_t)nodeId, pk, ad, (int)addrCount); (*env)->ReleaseByteArrayElements(env, addrsData, (jbyte*)ad, JNI_ABORT); (*env)->ReleaseByteArrayElements(env, pubkey, (jbyte*)pk, JNI_ABORT); } /* ── nativeSetEventCallback(eventCb) ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetEventCallback( JNIEnv* env, jobject thiz, jobject callback) { (void)thiz; if (g_event_callback) (*env)->DeleteGlobalRef(env, g_event_callback); g_event_callback = callback ? (*env)->NewGlobalRef(env, callback) : NULL; if (g_event_callback) { jclass cls = (*env)->GetObjectClass(env, g_event_callback); g_event_mid = (*env)->GetMethodID(env, cls, "onEvent", "(I[B)V"); if (!g_event_mid) bridge_log(BLEV_ERROR, "onEvent method not found on callback"); } else { g_event_mid = NULL; } } /* ── Pass-through JNI wrappers (unchanged from before) ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDestroy( JNIEnv* env, jobject thiz) { (void)thiz; udp_log_cancel_timer(); utun_bridge_destroy(); udp_log_stop(); if (g_config_callback) { (*env)->DeleteGlobalRef(env, g_config_callback); g_config_callback = NULL; } if (g_log_callback) { (*env)->DeleteGlobalRef(env, g_log_callback); g_log_callback = NULL; } if (g_event_callback) { (*env)->DeleteGlobalRef(env, g_event_callback); g_event_callback = NULL; } } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSendMessage( JNIEnv* env, jobject thiz, jstring channelId, jstring text) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); const char* tx = (*env)->GetStringUTFChars(env, text, NULL); utun_bridge_send_message(ch, tx); if (tx) (*env)->ReleaseStringUTFChars(env, text, tx); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCreateChannel( JNIEnv* env, jobject thiz, jstring name, jstring channelId) { (void)thiz; const char* n = (*env)->GetStringUTFChars(env, name, NULL); const char* c = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_create_channel(n, c); if (c) (*env)->ReleaseStringUTFChars(env, channelId, c); if (n) (*env)->ReleaseStringUTFChars(env, name, n); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectNode( JNIEnv* env, jobject thiz, jstring address, jint port, jstring pubkeyHex) { (void)thiz; const char* a = (*env)->GetStringUTFChars(env, address, NULL); const char* p = (*env)->GetStringUTFChars(env, pubkeyHex, NULL); utun_bridge_connect_node(a, (int)port, p); if (p) (*env)->ReleaseStringUTFChars(env, pubkeyHex, p); if (a) (*env)->ReleaseStringUTFChars(env, address, a); } JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetChannels( JNIEnv* env, jobject thiz) { (void)thiz; char* json = utun_bridge_get_channels_json(); jstring result = (*env)->NewStringUTF(env, json); free(json); return result; } JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMessages( JNIEnv* env, jobject thiz, jstring channelId, jint limit) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); char* json = utun_bridge_get_messages_json(ch, (int)limit); jstring result = (*env)->NewStringUTF(env, json); free(json); (*env)->ReleaseStringUTFChars(env, channelId, ch); return result; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectChannel( JNIEnv* env, jobject thiz, jstring channelId) { (void)thiz; const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); utun_bridge_connect_channel(ch); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetDebugLevel( JNIEnv* env, jobject thiz, jstring category, jstring level) { (void)thiz; const char* c = (*env)->GetStringUTFChars(env, category, NULL); const char* l = (*env)->GetStringUTFChars(env, level, NULL); utun_bridge_set_debug_level(c, l); if (l) (*env)->ReleaseStringUTFChars(env, level, l); if (c) (*env)->ReleaseStringUTFChars(env, category, c); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSubscribeLogs( JNIEnv* env, jobject thiz, jboolean enabled) { (void)thiz; (void)env; g_on_log = (enabled && g_log_callback) ? jni_on_log : NULL; } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetUdpLogTarget( JNIEnv* env, jobject thiz, jstring ip, jint port) { (void)thiz; const char* ip_str = (*env)->GetStringUTFChars(env, ip, NULL); udp_log_set_target(ip_str, (int)port); (*env)->ReleaseStringUTFChars(env, ip, ip_str); } JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRegenerateKeys( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; instance_lite_regenerate_keys(); } JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeClearDatabase( JNIEnv* env, jobject thiz) { (void)env; (void)thiz; if (!g_db_path[0]) { bridge_log(BLEV_ERROR, "clearDatabase: no db path"); return JNI_FALSE; } char db_file[512], wal_file[512], shm_file[512]; snprintf(db_file, sizeof(db_file), "%s/chats.db", g_db_path); snprintf(wal_file, sizeof(wal_file), "%s/chats.db-wal", g_db_path); snprintf(shm_file, sizeof(shm_file), "%s/chats.db-shm", g_db_path); unlink(db_file); unlink(wal_file); unlink(shm_file); bridge_log(BLEV_INFO, "clearDatabase: deleted DB files at %s", g_db_path); return JNI_TRUE; } #endif /* __ANDROID__ */