You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1503 lines
60 KiB

/*
* 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 "../libutun_lite/voice_recorder.h"
#include "../libutun_lite/attachment_sender.h"
#include "../../../lib/debug_config.h"
#include "../../../lib/u_async.h"
#include "../../../lib/mem.h"
#include "../../../lib/sqlite3.h"
#include "../../../src/chat/chat_core.h"
#include "../../../src/chat/chat_setting.h"
#include "../../../src/utun_instance.h"
#include "../../../src/transport_layer/etcp.h"
#include "../../../src/transport_layer/etcp_connections.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#ifdef __ANDROID__
#include <jni.h>
#include <android/log.h>
#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 char g_db_path[512];
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);
voice_recorder_init(db_path);
return true;
}
void utun_bridge_destroy(void) {
voice_recorder_deinit();
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");
}
int 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 thread created");
}
return rc;
}
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 : "");
if (!channel_id || !text) return;
struct UASYNC* ua = instance_lite_get_uasync();
if (!ua) { bridge_log(BLEV_ERROR, "send: ua not ready"); return; }
size_t datalen = strlen(text);
struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + datalen);
if (!req) return;
strncpy(req->channel_id, channel_id, sizeof(req->channel_id) - 1);
strncpy(req->content_type, "text/plain", sizeof(req->content_type) - 1);
req->data = (uint8_t*)(req + 1);
req->data_len = (uint32_t)datalen;
memcpy(req->data, text, datalen);
req->timestamp = 0;
uasync_post(ua, chat_core_submit_trampoline, req);
bridge_log(BLEV_DEBUG, "send queued ch=%s len=%zu", channel_id, datalen);
}
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,
int addrs_data_len) {
bridge_log(BLEV_INFO, "join ch=%llu node=0x%016llx addrs=%d len=%d",
(unsigned long long)channel_id, (unsigned long long)node_id, addr_count, addrs_data_len);
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, addrs_data_len);
}
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);
}
#include "chat/chat_core_priv.h"
static char* json_escape_alloc(const char* str) {
if (!str) return u_strdup("");
size_t len = strlen(str), cap = len * 2 + 8;
char* out = u_malloc(cap);
if (!out) return u_strdup("");
size_t pos = 0;
for (size_t i = 0; i < len && pos < cap - 2; i++) {
char c = str[i];
if (c == '"' || c == '\\') { out[pos++] = '\\'; out[pos++] = c; }
else if (c == '\n') { out[pos++] = '\\'; out[pos++] = 'n'; }
else if (c == '\r') { out[pos++] = '\\'; out[pos++] = 'r'; }
else if (c == '\t') { out[pos++] = '\\'; out[pos++] = 't'; }
else if ((unsigned char)c < 32) { pos += snprintf(out + pos, cap - pos, "\\u%04x", (unsigned char)c); }
else out[pos++] = c;
}
out[pos] = '\0';
return out;
}
static void bridge_get_node_name(sqlite3* db, uint64_t node_id, char* out, size_t sz) {
out[0] = '\0';
if (!db || node_id == 0) return;
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* n = (const char*)sqlite3_column_text(st, 0);
if (n && n[0]) snprintf(out, sz, "%s", n);
}
sqlite3_finalize(st);
}
char* utun_bridge_get_channels_json(void) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db) {
bridge_log(BLEV_WARN, "get_channels: not initialized db=%p initialized=%d",
(void*)db, chat_core_is_initialized());
return u_strdup("[]");
}
size_t cap = 4096, pos = 1;
char* json = u_malloc(cap);
if (!json) return u_strdup("[]");
json[0] = '[';
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(db, "SELECT channel_id, name, last_msg_at FROM channels ORDER BY last_msg_at DESC, name ASC", -1, &st, NULL);
if (rc != SQLITE_OK) { u_free(json); return u_strdup("[]"); }
int first = 1;
while (sqlite3_step(st) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(st, 0);
const char* name = (const char*)sqlite3_column_text(st, 1);
int64_t last_msg_at = sqlite3_column_int64(st, 2);
if (!ch_id) continue;
char tbl_msg[80], tbl_peers[80], last_msg[128];
msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg));
peers_table_name(ch_id, tbl_peers, sizeof(tbl_peers));
last_msg[0] = '\0';
sqlite3_stmt* lm = NULL;
char sql[256];
snprintf(sql, sizeof(sql), "SELECT node_id, data FROM \"%s\" ORDER BY id DESC LIMIT 1", tbl_msg);
if (sqlite3_prepare_v2(db, sql, -1, &lm, NULL) == SQLITE_OK) {
if (sqlite3_step(lm) == SQLITE_ROW) {
int64_t author = sqlite3_column_int64(lm, 0);
const uint8_t* dptr = sqlite3_column_blob(lm, 1);
int dlen = sqlite3_column_bytes(lm, 1);
char txt[128] = "";
if (dptr && dlen > 0) {
const char* dp = (const char*)dptr;
const char* ds = strstr(dp, "\"d\":\"");
if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 120) tl = 120; memcpy(txt, ds, tl); txt[tl] = '\0'; } }
}
char author_name[128];
bridge_get_node_name(db, (uint64_t)author, author_name, sizeof(author_name));
if (!author_name[0])
snprintf(author_name, sizeof(author_name), "0x%016llx", (unsigned long long)author);
snprintf(last_msg, sizeof(last_msg), "%s: %s", author_name, txt);
}
sqlite3_finalize(lm);
}
int online = 0;
uint64_t my_id = g_cc.my_node_id;
struct UTUN_INSTANCE* inst = chat_core_get_inst();
sqlite3_stmt* pc = NULL;
snprintf(sql, sizeof(sql), "SELECT DISTINCT node_id FROM \"%s\"", tbl_peers);
if (sqlite3_prepare_v2(db, sql, -1, &pc, NULL) == SQLITE_OK) {
while (sqlite3_step(pc) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(pc, 0);
if (nid == my_id) continue;
if (inst) { struct ETCP_CONN* c = instance_find_conn(inst, nid); if (c && c->initialized && c->links_up) online++; }
else online++;
}
sqlite3_finalize(pc);
}
char* esc_name = json_escape_alloc(name ? name : ch_id);
char* esc_msg = json_escape_alloc(last_msg);
const char* sep = first ? "" : ",";
first = 0;
size_t needed = snprintf(NULL, 0, "%s{\"id\":\"%s\",\"name\":\"%s\",\"lastMsgAt\":%lld,\"lastMsg\":\"%s\",\"peersOnline\":%d}",
sep, ch_id, esc_name, (long long)last_msg_at, esc_msg, online);
while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; }
if (pos + needed + 2 <= cap)
pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"id\":\"%s\",\"name\":\"%s\",\"lastMsgAt\":%lld,\"lastMsg\":\"%s\",\"peersOnline\":%d}",
sep, ch_id, esc_name, (long long)last_msg_at, esc_msg, online);
u_free(esc_msg);
u_free(esc_name);
}
sqlite3_finalize(st);
json[pos++] = ']'; json[pos] = '\0';
return json;
}
char* utun_bridge_get_messages_json(const char* channel_id, int limit) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db || !channel_id || !channel_id[0]) return u_strdup("[]");
uint64_t my_id = g_cc.my_node_id;
char tbl_msg[80];
msg_table_name(channel_id, tbl_msg, sizeof(tbl_msg));
char sql[256];
snprintf(sql, sizeof(sql), "SELECT id, node_id, data, timestamp, local_attrs FROM \"%s\" ORDER BY timestamp ASC LIMIT %d", tbl_msg, limit);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return u_strdup("[]");
size_t cap = 4096, pos = 1;
char* json = u_malloc(cap);
if (!json) { sqlite3_finalize(st); return u_strdup("[]"); }
json[0] = '[';
int first = 1;
while (sqlite3_step(st) == SQLITE_ROW) {
int64_t msg_id = sqlite3_column_int64(st, 0);
int64_t node_id = sqlite3_column_int64(st, 1);
int64_t ts = sqlite3_column_int64(st, 3);
const uint8_t* dptr = sqlite3_column_blob(st, 2);
int dlen = sqlite3_column_bytes(st, 2);
const char* lattr = (const char*)sqlite3_column_text(st, 4);
char txt[2048] = "", ct[64] = "text/plain";
if (dptr && dlen > 0) {
const char* ds = strstr((const char*)dptr, "\"d\":\"");
if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 2000) tl = 2000; memcpy(txt, ds, tl); txt[tl] = '\0'; } }
const char* cts = strstr((const char*)dptr, "\"ct\":\"");
if (cts) { cts += 6; char* cte = strchr((char*)cts, '"'); if (cte) { size_t tlc = (size_t)(cte - cts); if (tlc < sizeof(ct)) { memcpy(ct, cts, tlc); ct[tlc] = '\0'; } } }
}
char* esc_txt = json_escape_alloc(txt);
char* esc_ct = json_escape_alloc(ct);
int is_out = (node_id == (int64_t)my_id) ? 1 : 0;
const char* sep = first ? "" : ",";
first = 0;
char author_name[128];
bridge_get_node_name(db, (uint64_t)node_id, author_name, sizeof(author_name));
if (!author_name[0])
snprintf(author_name, sizeof(author_name), "0x%016llx", (unsigned long long)node_id);
/* build filePath for voice/attachment messages */
char file_path[1024] = "";
if (lattr) {
const char* fps = strstr(lattr, "\"fp\":\"");
if (fps) { fps += 6;
char* fpe = strchr((char*)fps, '"');
if (fpe) { size_t fpl = (size_t)(fpe - fps);
snprintf(file_path, sizeof(file_path), "%s/media/%s/%.*s",
g_db_path, channel_id, (int)fpl, fps);
}
}
}
char* esc_fp = file_path[0] ? json_escape_alloc(file_path) : NULL;
char* esc_la = lattr && lattr[0] ? json_escape_alloc(lattr) : NULL;
size_t needed = snprintf(NULL, 0, "%s{\"id\":%lld,\"author\":\"%s\",\"text\":\"%s\",\"ts\":%lld,\"isOutgoing\":%s,\"contentType\":\"%s\"%s%s%s%s%s%s}",
sep, (long long)msg_id, author_name, esc_txt, (long long)ts, is_out ? "true" : "false", esc_ct,
esc_fp ? ",\"filePath\":\"" : "", esc_fp ? esc_fp : "", esc_fp ? "\"" : "",
esc_la ? ",\"localAttrs\":\"" : "", esc_la ? esc_la : "", esc_la ? "\"" : "");
while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; }
if (pos + needed + 2 <= cap)
pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"id\":%lld,\"author\":\"%s\",\"text\":\"%s\",\"ts\":%lld,\"isOutgoing\":%s,\"contentType\":\"%s\"%s%s%s%s%s%s}",
sep, (long long)msg_id, author_name, esc_txt, (long long)ts, is_out ? "true" : "false", esc_ct,
esc_fp ? ",\"filePath\":\"" : "", esc_fp ? esc_fp : "", esc_fp ? "\"" : "",
esc_la ? ",\"localAttrs\":\"" : "", esc_la ? esc_la : "", esc_la ? "\"" : "");
if (esc_la) u_free(esc_la);
if (esc_fp) u_free(esc_fp);
u_free(esc_ct);
u_free(esc_txt);
}
sqlite3_finalize(st);
json[pos++] = ']'; json[pos] = '\0';
return json;
}
uint64_t utun_bridge_get_my_node_id(void) {
return chat_core_is_initialized() ? g_cc.my_node_id : 0;
}
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");
}
void utun_bridge_chat_setting_set(const char* name, const char* value) {
chat_setting_set(name, value);
}
int utun_bridge_chat_setting_get_int(const char* name, int def) {
return chat_setting_get_int(name, def);
}
/* ──────────────────────────────────────────────────────────────────
* Channel members (queried from shared DB)
* ────────────────────────────────────────────────────────────────── */
#include "../../../src/routing_layer/topo_node_sqlite.h"
#include <openssl/evp.h>
char* utun_bridge_get_channel_members_json(const char* channel_id) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db || !channel_id || !channel_id[0]) return u_strdup("[]");
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
sqlite3_stmt* cnt_st = NULL;
char sql[512];
snprintf(sql, sizeof(sql), "SELECT COUNT(*) FROM \"%s\"", peers_tbl);
/* if table doesn't exist, return empty */
if (sqlite3_prepare_v2(db, sql, -1, &cnt_st, NULL) != SQLITE_OK) return u_strdup("[]");
int member_count = 0;
if (sqlite3_step(cnt_st) == SQLITE_ROW) member_count = sqlite3_column_int(cnt_st, 0);
sqlite3_finalize(cnt_st);
if (member_count == 0) return u_strdup("[]");
snprintf(sql, sizeof(sql),
"SELECT p.node_id, COALESCE(n.online,0), COALESCE(n.name,''),"
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
" COALESCE(p.node_type,0), COALESCE(p.node_RTT,0), COALESCE(p.storage,0)"
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" ORDER BY n.online DESC, p.node_id ASC", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
bridge_log(BLEV_ERROR, "get_channel_members: main prepare failed ch=%s err=%s",
channel_id, sqlite3_errmsg(db));
return u_strdup("[]");
}
size_t cap = 4096, pos = 1;
char* json = u_malloc(cap);
if (!json) { sqlite3_finalize(st); return u_strdup("[]"); }
json[0] = '[';
uint64_t my_id = g_cc.my_node_id;
int first = 1;
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t node_id = (uint64_t)sqlite3_column_int64(st, 0);
int online = sqlite3_column_int(st, 1);
const char* name = (const char*)sqlite3_column_text(st, 2);
const char* local_nick = (const char*)sqlite3_column_text(st, 3);
const char* adm_tags = (const char*)sqlite3_column_text(st, 4);
int node_type = sqlite3_column_int(st, 5);
int node_rtt = sqlite3_column_int(st, 6);
int storage_flag = sqlite3_column_int(st, 7);
int is_admin = 0, is_super = 0;
if (adm_tags) {
if (strstr(adm_tags, "admin=yes")) is_admin = 1;
if (strstr(adm_tags, "supernode=yes")) is_super = 1;
}
char* esc_name = json_escape_alloc(local_nick && local_nick[0] ? local_nick :
(name && name[0] ? name : ""));
const char* sep = first ? "" : ",";
first = 0;
size_t needed = snprintf(NULL, 0,
"%s{\"n\":\"%s\",\"ni\":%llu,\"on\":%d,\"ad\":%d,\"sp\":%d,\"st\":%d,"
"\"tp\":%d,\"rt\":%d,\"is\":%d}",
sep, esc_name, (unsigned long long)node_id, online,
is_admin, is_super, storage_flag,
node_type, node_rtt, (node_id == my_id) ? 1 : 0);
while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; }
if (pos + needed + 2 <= cap)
pos += (size_t)snprintf(json + pos, cap - pos,
"%s{\"n\":\"%s\",\"ni\":%llu,\"on\":%d,\"ad\":%d,\"sp\":%d,\"st\":%d,"
"\"tp\":%d,\"rt\":%d,\"is\":%d}",
sep, esc_name, (unsigned long long)node_id, online,
is_admin, is_super, storage_flag,
node_type, node_rtt, (node_id == my_id) ? 1 : 0);
u_free(esc_name);
}
sqlite3_finalize(st);
json[pos++] = ']'; json[pos] = '\0';
return json;
}
char* utun_bridge_get_member_detail_json(const char* channel_id, uint64_t node_id) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db || !channel_id || node_id == 0) return u_strdup("{}");
char buf[8192];
size_t pos = 0;
pos += (size_t)snprintf(buf + pos, sizeof(buf) - pos, "{\"ni\":%llu", (unsigned long long)node_id);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db,
"SELECT COALESCE(name,''), COALESCE(online,0),"
" COALESCE(last_seen_at,0), COALESCE(created_at,0)"
" FROM nodes WHERE node_id=?", -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* name = (const char*)sqlite3_column_text(st, 0);
int online = sqlite3_column_int(st, 1);
int64_t last_seen = sqlite3_column_int64(st, 2);
int64_t created = sqlite3_column_int64(st, 3);
char* esc_name = json_escape_alloc(name && name[0] ? name : "");
pos += (size_t)snprintf(buf + pos, sizeof(buf) - pos,
",\"n\":\"%s\",\"on\":%d,\"ls\":%lld,\"cr\":%lld",
esc_name, online, (long long)last_seen, (long long)created);
u_free(esc_name);
}
sqlite3_finalize(st);
}
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
snprintf(buf + pos, sizeof(buf) - pos, ",\"adr\":["); pos += strlen(buf + pos);
if (sqlite3_prepare_v2(db,
"SELECT family, protocol, address, port, socket_id, COALESCE(rtt,0)"
" FROM node_addresses WHERE node_id=? ORDER BY family, port",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
int af = 1;
while (sqlite3_step(st) == SQLITE_ROW) {
int family = sqlite3_column_int(st, 0);
int proto = sqlite3_column_int(st, 1);
const uint8_t* addr = (const uint8_t*)sqlite3_column_blob(st, 2);
int addr_len = sqlite3_column_bytes(st, 2);
int port = sqlite3_column_int(st, 3);
int sid = sqlite3_column_int(st, 4);
int rtt = sqlite3_column_int(st, 5);
char addr_str[64] = "";
if (addr && addr_len >= (family == 4 ? 4 : 16)) {
if (family == 4)
snprintf(addr_str, sizeof(addr_str), "%d.%d.%d.%d", addr[0], addr[1], addr[2], addr[3]);
else if (family == 6) {
const uint16_t* a = (const uint16_t*)addr;
snprintf(addr_str, sizeof(addr_str), "%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x",
ntohs(a[0]), ntohs(a[1]), ntohs(a[2]), ntohs(a[3]),
ntohs(a[4]), ntohs(a[5]), ntohs(a[6]), ntohs(a[7]));
}
}
const char* pn;
switch (proto) { case 1: pn = "UDP"; break; case 2: pn = "TCP"; break; default: pn = "UDP/TCP"; break; }
pos += (size_t)snprintf(buf + pos, sizeof(buf) - pos,
"%s{\"fm\":%d,\"pt\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"si\":%d,\"rt\":%d}",
af ? "" : ",", family, pn, addr_str[0] ? addr_str : "?", port, sid, rtt);
af = 0;
}
sqlite3_finalize(st);
}
pos += (size_t)snprintf(buf + pos, sizeof(buf) - pos, "]}");
return u_strdup(buf);
}
void utun_bridge_set_member_nick(const char* channel_id, uint64_t node_id, const char* nick) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db || !channel_id || node_id == 0) return;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
char sql[256];
snprintf(sql, sizeof(sql), "UPDATE \"%s\" SET local_nick=? WHERE node_id=?", peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) {
if (nick && nick[0])
sqlite3_bind_text(st, 1, nick, -1, SQLITE_STATIC);
else
sqlite3_bind_null(st, 1);
sqlite3_bind_int64(st, 2, (sqlite3_int64)node_id);
sqlite3_step(st);
sqlite3_finalize(st);
}
bridge_log(BLEV_DEBUG, "set_member_nick ch=%s nid=%llu nick=%s",
channel_id, (unsigned long long)node_id, nick ? nick : "(null)");
}
void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id,
int admin, int supernode, int storage_flag) {
sqlite3* db = chat_core_get_db();
if (!chat_core_is_initialized() || !db || !channel_id || node_id == 0) return;
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
/* current version */
int cur_ver = 0;
sqlite3_stmt* vst = NULL;
char sql[256]; snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", peers_tbl);
if (sqlite3_prepare_v2(db, sql, -1, &vst, NULL) == SQLITE_OK) {
sqlite3_bind_int64(vst, 1, (sqlite3_int64)node_id);
if (sqlite3_step(vst) == SQLITE_ROW) {
const char* tags = (const char*)sqlite3_column_text(vst, 0);
if (tags) { const char* vp = strstr(tags, "ver="); if (vp) cur_ver = atoi(vp + 4); }
}
sqlite3_finalize(vst);
}
int new_ver = cur_ver + 1;
char adm_tags[128] = "";
int off = 0;
off += snprintf(adm_tags + off, sizeof(adm_tags) - (size_t)off, "ver=%d", new_ver);
if (admin) off += snprintf(adm_tags + off, sizeof(adm_tags) - (size_t)off, ",admin=yes");
if (supernode) off += snprintf(adm_tags + off, sizeof(adm_tags) - (size_t)off, ",supernode=yes");
if (storage_flag) off += snprintf(adm_tags + off, sizeof(adm_tags) - (size_t)off, ",storage=yes");
/* get channel ed25519 private key for signing */
uint8_t ch_ed_priv[32] = {0};
int have_key = 0;
sqlite3_stmt* ks = NULL;
if (sqlite3_prepare_v2(db,
"SELECT ed25519_privkey FROM channels WHERE channel_id=?",
-1, &ks, NULL) == SQLITE_OK) {
sqlite3_bind_text(ks, 1, channel_id, -1, SQLITE_STATIC);
if (sqlite3_step(ks) == SQLITE_ROW) {
const uint8_t* kp = (const uint8_t*)sqlite3_column_blob(ks, 0);
if (kp && sqlite3_column_bytes(ks, 0) >= 32) {
memcpy(ch_ed_priv, kp, 32); have_key = 1;
}
}
sqlite3_finalize(ks);
}
/* sign with Ed25519 */
uint8_t sig[64] = {0};
if (have_key) {
uint8_t msg[384]; size_t mlen = 0;
size_t tl = strlen(adm_tags);
if (tl > 191) tl = 191;
memcpy(msg + mlen, adm_tags, tl); mlen += tl;
memcpy(msg + mlen, &node_id, 8); mlen += 8;
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, ch_ed_priv, 32);
if (pkey) {
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (mdctx) {
size_t siglen = 64;
if (EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) == 1)
EVP_DigestSign(mdctx, sig, &siglen, msg, mlen);
EVP_MD_CTX_free(mdctx);
}
EVP_PKEY_free(pkey);
}
}
int nt = supernode ? 4 : 1;
snprintf(sql, sizeof(sql),
"UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, node_type=?, storage=? WHERE node_id=?",
peers_tbl);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, adm_tags, -1, SQLITE_STATIC);
if (have_key) sqlite3_bind_blob(st, 2, sig, 64, SQLITE_STATIC);
else sqlite3_bind_null(st, 2);
sqlite3_bind_int(st, 3, nt);
sqlite3_bind_int(st, 4, storage_flag ? 1 : 0);
sqlite3_bind_int64(st, 5, (sqlite3_int64)node_id);
sqlite3_step(st);
sqlite3_finalize(st);
}
bridge_log(BLEV_INFO, "set_member_flags ch=%s nid=%llu admin=%d super=%d storage=%d tags=%s have_key=%d",
channel_id, (unsigned long long)node_id, admin, supernode, storage_flag, adm_tags, have_key);
}
char* utun_bridge_get_member_links_json(uint64_t node_id) {
struct UTUN_INSTANCE* inst = chat_core_get_inst();
if (!inst || node_id == 0) return u_strdup("{}");
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
if (!conn) return u_strdup("{}");
char buf[4096];
int pos = snprintf(buf, sizeof(buf),
"{\"cn\":true,\"in\":%s,\"lu\":%d,\"lk\":[",
conn->initialized ? "true" : "false", conn->links_up);
int first = 1;
for (struct ETCP_LINK* lk = conn->links; lk; lk = lk->next) {
if (pos >= (int)sizeof(buf) - 100) break;
const struct sockaddr_storage* sa = &lk->remote_addr;
char ip[INET6_ADDRSTRLEN] = "";
int fm = 0;
uint16_t po = 0;
if (sa->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
inet_ntop(AF_INET, &sin->sin_addr, ip, sizeof(ip));
fm = 4; po = ntohs(sin->sin_port);
} else if (sa->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)sa;
inet_ntop(AF_INET6, &sin6->sin6_addr, ip, sizeof(ip));
fm = 6; po = ntohs(sin6->sin6_port);
} else continue;
if (!ip[0]) continue;
const char* sep = first ? "" : ","; first = 0;
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos,
"%s{\"fm\":%d,\"ad\":\"%s\",\"po\":%d,\"st\":%d,\"ls\":%d}",
sep, fm, ip, (int)po, lk->link_state, lk->link_status);
}
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "]}");
return u_strdup(buf);
}
void utun_bridge_restart(const char* config_text) {
bridge_log(BLEV_INFO, "bridge restart: posting to instance");
instance_lite_restart(config_text);
}
void utun_bridge_ping(void) {
instance_lite_ping();
}
int utun_bridge_is_responsive(void) {
return instance_lite_is_responsive();
}
/* ──────────────────────────────────────────────────────────────────
* Voice recording
* ────────────────────────────────────────────────────────────────── */
int utun_bridge_voice_start(const char* channel_id) {
return voice_recorder_start(channel_id, 48000, 1);
}
int utun_bridge_voice_feed(const int16_t* samples, int count) {
return voice_recorder_feed(samples, count);
}
int utun_bridge_voice_stop(int* out_duration_ms) {
return voice_recorder_stop(out_duration_ms);
}
void utun_bridge_voice_cancel(void) {
voice_recorder_cancel();
}
int utun_bridge_voice_is_active(void) {
return voice_recorder_is_active();
}
void utun_bridge_voice_set_preset(int preset) {
voice_recorder_set_preset(preset);
}
int utun_bridge_voice_get_preset(void) {
return voice_recorder_get_preset();
}
void utun_bridge_voice_set_compressor(int enabled) {
voice_recorder_set_compressor_enabled(enabled);
}
int utun_bridge_voice_get_compressor(void) {
return voice_recorder_is_compressor_enabled();
}
void utun_bridge_voice_set_compressor_config(int max_gain_db, int lookback_ms, int lookahead_ms,
float rise_rate_per_sec, float target_level_db) {
voice_recorder_set_compressor_config(max_gain_db, lookback_ms, lookahead_ms, rise_rate_per_sec, target_level_db);
}
float utun_bridge_voice_get_peak_level(void) {
return voice_recorder_get_peak_level();
}
int utun_bridge_attachment_send(const char* channel_id, const char* file_path, const char* db_path) {
return attachment_send(channel_id, file_path, db_path);
}
int utun_bridge_attachment_download(const char* channel_id, int64_t msg_id) {
if (!channel_id || msg_id <= 0) { bridge_log(BLEV_ERROR, "attachment_download: invalid args"); return -1; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!ua) { bridge_log(BLEV_ERROR, "attachment_download: no uasync"); return -1; }
struct attachment_dl_req* req = u_calloc(1, sizeof(*req));
if (!req) return -1;
snprintf(req->channel_id, sizeof(req->channel_id), "%s", channel_id);
req->msg_id = msg_id;
bridge_log(BLEV_INFO, "attachment_download: ch=%s msg_id=%lld", channel_id, (long long)msg_id);
uasync_post(ua, chat_core_attachment_download_trampoline, req);
return 0;
}
/* ──────────────────────────────────────────────────────────────────
* Voice playback (Opus decode)
* ────────────────────────────────────────────────────────────────── */
#include "../../../lib/opus_codec.h"
struct voice_decode_handle {
opus_codec_decoder_t* dec;
FILE* file;
int sample_rate;
int channels;
int frame_samples;
int total_samples;
int pcm_consumed;
};
#define OPUS_MAGIC_D 0x5355504F
void* utun_bridge_voice_decode_open(const char* file_path,
int* out_sample_rate, int* out_channels,
int* out_total_samples) {
if (!file_path || !out_sample_rate || !out_channels || !out_total_samples) return NULL;
FILE* f = fopen(file_path, "rb");
if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_decode: cannot open %s", file_path); return NULL; }
uint32_t magic = 0; uint32_t sr = 0; uint16_t ch = 0, fs = 0;
if (fread(&magic, 4, 1, f) != 1 || magic != OPUS_MAGIC_D) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "voice_decode: bad magic"); fclose(f); return NULL; }
if (fread(&sr, 4, 1, f) != 1) { fclose(f); return NULL; }
if (fread(&ch, 2, 1, f) != 1) { fclose(f); return NULL; }
if (fread(&fs, 2, 1, f) != 1) { fclose(f); return NULL; }
opus_codec_decoder_t* dec = opus_codec_decoder_create((int)sr, (int)ch);
if (!dec) { fclose(f); return NULL; }
struct voice_decode_handle* h = u_calloc(1, sizeof(*h));
if (!h) { opus_codec_decoder_destroy(dec); fclose(f); return NULL; }
h->dec = dec; h->file = f;
h->sample_rate = (int)sr; h->channels = (int)ch; h->frame_samples = (int)fs;
/* count total frames/samples */
long pos = ftell(f);
h->total_samples = 0;
for (;;) {
uint16_t plen;
if (fread(&plen, 2, 1, f) != 1 || plen == 0) break;
fseek(f, plen, SEEK_CUR);
h->total_samples += (int)fs * (int)ch;
}
fseek(f, pos, SEEK_SET);
*out_sample_rate = h->sample_rate;
*out_channels = h->channels;
*out_total_samples = h->total_samples;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "voice_decode_open: %s rate=%d ch=%d fs=%d total=%d",
file_path, h->sample_rate, h->channels, h->frame_samples, h->total_samples);
return h;
}
int utun_bridge_voice_decode_read(void* handle, int16_t* buf, int max_samples) {
struct voice_decode_handle* h = (struct voice_decode_handle*)handle;
if (!h || !buf || max_samples <= 0) return -1;
if (h->pcm_consumed >= h->total_samples) return 0;
uint8_t packet[4096];
int written = 0;
while (written + h->frame_samples * h->channels <= max_samples) {
uint16_t plen;
if (fread(&plen, 2, 1, h->file) != 1 || plen == 0) break;
if (plen > sizeof(packet)) { fseek(h->file, plen, SEEK_CUR); break; }
if (fread(packet, 1, plen, h->file) != plen) break;
int decoded = opus_codec_decode(h->dec, packet, plen, buf + written, h->frame_samples);
if (decoded < 0) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "voice_decode_read: error %d", decoded); break; }
written += decoded * h->channels;
}
h->pcm_consumed += written;
return written;
}
void utun_bridge_voice_decode_close(void* handle) {
if (!handle) return;
struct voice_decode_handle* h = (struct voice_decode_handle*)handle;
if (h->dec) opus_codec_decoder_destroy(h->dec);
if (h->file) fclose(h->file);
u_free(h);
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "voice_decode_close");
}
/* ──────────────────────────────────────────────────────────────────
* 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;
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);
int rc = utun_bridge_start(cfg);
(*env)->ReleaseStringUTFChars(env, configText, cfg);
return (rc >= 0) ? 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, (int)ad_len);
(*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);
u_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);
u_free(json);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
return result;
}
JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMyNodeId(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return (jlong)utun_bridge_get_my_node_id();
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRestart(
JNIEnv* env, jobject thiz, jstring configText) {
(void)thiz;
if (!configText) return JNI_FALSE;
const char* cfg = (*env)->GetStringUTFChars(env, configText, NULL);
if (!cfg) return JNI_FALSE;
utun_bridge_restart(cfg);
(*env)->ReleaseStringUTFChars(env, configText, cfg);
return JNI_TRUE;
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeIsResponsive(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return utun_bridge_is_responsive() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativePing(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
utun_bridge_ping();
}
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_nativeSetChatSetting(
JNIEnv* env, jobject thiz, jstring name, jstring value) {
(void)thiz;
const char* n = (*env)->GetStringUTFChars(env, name, NULL);
const char* v = (*env)->GetStringUTFChars(env, value, NULL);
chat_setting_set(n, v);
if (v) (*env)->ReleaseStringUTFChars(env, value, v);
if (n) (*env)->ReleaseStringUTFChars(env, name, n);
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeGetChatSettingInt(
JNIEnv* env, jobject thiz, jstring name, jint def) {
(void)thiz;
const char* n = (*env)->GetStringUTFChars(env, name, NULL);
int val = chat_setting_get_int(n, (int)def);
if (n) (*env)->ReleaseStringUTFChars(env, name, n);
return (jint)val;
}
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;
}
/* ── Voice recording JNI ── */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceStart(
JNIEnv* env, jobject thiz, jstring channelId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
int rc = utun_bridge_voice_start(ch);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
return (rc == 0) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceFeed(
JNIEnv* env, jobject thiz, jshortArray samples) {
(void)env; (void)thiz;
if (!samples) return JNI_FALSE;
jsize len = (*env)->GetArrayLength(env, samples);
jshort* data = (*env)->GetShortArrayElements(env, samples, NULL);
int rc = utun_bridge_voice_feed((const int16_t*)data, (int)len);
(*env)->ReleaseShortArrayElements(env, samples, data, JNI_ABORT);
return (rc == 0) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceStop(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
int duration_ms = 0;
utun_bridge_voice_stop(&duration_ms);
return (jint)duration_ms;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceCancel(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
utun_bridge_voice_cancel();
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceIsActive(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return utun_bridge_voice_is_active() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceSetPreset(
JNIEnv* env, jobject thiz, jint preset) {
(void)env; (void)thiz;
utun_bridge_voice_set_preset((int)preset);
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceGetPreset(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return (jint)utun_bridge_voice_get_preset();
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceSetCompressor(
JNIEnv* env, jobject thiz, jboolean enabled) {
(void)env; (void)thiz;
utun_bridge_voice_set_compressor(enabled ? 1 : 0);
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceGetCompressor(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return utun_bridge_voice_get_compressor() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceSetCompressorConfig(
JNIEnv* env, jobject thiz, jint maxGainDb, jint lookbackMs, jint lookaheadMs,
jfloat riseRate, jfloat targetLevelDb) {
(void)env; (void)thiz;
utun_bridge_voice_set_compressor_config((int)maxGainDb, (int)lookbackMs, (int)lookaheadMs,
(float)riseRate, (float)targetLevelDb);
}
JNIEXPORT jfloat JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceGetPeakLevel(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return (jfloat)utun_bridge_voice_get_peak_level();
}
/* ── Attachment JNI ── */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeAttachmentSend(
JNIEnv* env, jobject thiz, jstring channelId, jstring filePath) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
const char* fp = (*env)->GetStringUTFChars(env, filePath, NULL);
int rc = utun_bridge_attachment_send(ch, fp, g_db_path);
if (fp) (*env)->ReleaseStringUTFChars(env, filePath, fp);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
return (rc == 0) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeAttachmentDownload(
JNIEnv* env, jobject thiz, jstring channelId, jlong msgId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
int rc = utun_bridge_attachment_download(ch, (int64_t)msgId);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
return (rc == 0) ? JNI_TRUE : JNI_FALSE;
}
/* ── Voice playback JNI ── */
JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceDecodeOpen(
JNIEnv* env, jobject thiz, jstring filePath, jintArray outInfo) {
(void)thiz;
if (!filePath || !outInfo) return 0;
const char* fp = (*env)->GetStringUTFChars(env, filePath, NULL);
int sr = 0, ch = 0, total = 0;
void* h = utun_bridge_voice_decode_open(fp, &sr, &ch, &total);
if (h) {
jint info[3] = {sr, ch, total};
(*env)->SetIntArrayRegion(env, outInfo, 0, 3, info);
}
if (fp) (*env)->ReleaseStringUTFChars(env, filePath, fp);
return (jlong)(intptr_t)h;
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceDecodeRead(
JNIEnv* env, jobject thiz, jlong handle, jshortArray buf) {
(void)thiz;
if (handle == 0 || !buf) return -1;
jsize max = (*env)->GetArrayLength(env, buf);
jshort* data = (*env)->GetShortArrayElements(env, buf, NULL);
int read = utun_bridge_voice_decode_read((void*)(intptr_t)handle, (int16_t*)data, (int)max);
(*env)->ReleaseShortArrayElements(env, buf, data, 0);
return (jint)read;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceDecodeClose(
JNIEnv* env, jobject thiz, jlong handle) {
(void)env; (void)thiz;
utun_bridge_voice_decode_close((void*)(intptr_t)handle);
}
/* ── Channel members JNI ── */
JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetChannelMembers(
JNIEnv* env, jobject thiz, jstring channelId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
char* json = utun_bridge_get_channel_members_json(ch);
jstring result = (*env)->NewStringUTF(env, json);
u_free(json);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
return result;
}
JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMemberDetail(
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
char* json = utun_bridge_get_member_detail_json(ch, (uint64_t)nodeId);
jstring result = (*env)->NewStringUTF(env, json);
u_free(json);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
return result;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetMemberNick(
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId, jstring nick) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
const char* nk = nick ? (*env)->GetStringUTFChars(env, nick, NULL) : NULL;
utun_bridge_set_member_nick(ch, (uint64_t)nodeId, nk);
if (nk) (*env)->ReleaseStringUTFChars(env, nick, nk);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetMemberFlags(
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId,
jboolean admin, jboolean supernode, jboolean storage) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_set_member_flags(ch, (uint64_t)nodeId, admin, supernode, storage);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
}
JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMemberLinks(
JNIEnv* env, jobject thiz, jlong nodeId) {
(void)thiz;
char* json = utun_bridge_get_member_links_json((uint64_t)nodeId);
jstring result = (*env)->NewStringUTF(env, json);
u_free(json);
return result;
}
#endif /* __ANDROID__ */