Files
utun2/tools/chatgui-android/jni_bridge/android_jni_bridge.c
T

3141 lines
148 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 "../libutun_lite/video_sender.h"
#include "../libutun_lite/photo_sender.h"
#include "../libutun_lite/standby.h"
#include "../../../src/call/call_audio.h"
#include "../../../src/call/call.h"
#include "../../../src/radio/radio.h"
#include "../../../src/radio/radio_audio.h"
#include "../../../src/dm/dm_core.h"
#include "../../../src/dm/dm_media.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_poll.h"
#include "../../../lib/strbuf.h"
#include "../../../src/chat/chat_member.h"
#include "../../../src/chat/group_avatar.h"
#include "../../../src/member_files/member_files.h"
#include "../../../src/chat/invite_build.h"
#include "../../../src/chat/chat_setting.h"
#include "../../../src/routing_layer/topo_node_sqlite.h"
#include "../../../src/utun_instance.h"
#include "../../../src/transport_layer/etcp.h"
#include "../../../src/transport_layer/etcp_connections.h"
#include "../../../src/transport_layer/auto_socket.h"
#include "../../../src/transport_layer/secure_channel.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdarg.h>
#include <limits.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;
char fmt_buf[1024];
va_start(ap, fmt);
vsnprintf(fmt_buf, sizeof(fmt_buf), fmt, ap);
va_end(ap);
#ifdef __ANDROID__
__android_log_print(blevel_to_android(level), LOG_TAG, "%s", fmt_buf);
#else
fprintf(stderr, "%s\n", fmt_buf);
#endif
if (g_on_log) g_on_log(level, "JNI", fmt_buf);
}
static void bridge_debug_hook(int level, const char* category, const char* message) {
/* DEBUG_*: ERROR=1..TRACE=5; bridge/Kotlin: TRACE=0..ERROR=4. */
int bridge_level = level >= DEBUG_LEVEL_ERROR && level <= DEBUG_LEVEL_TRACE ? DEBUG_LEVEL_TRACE - level : BLEV_DEBUG;
#ifdef __ANDROID__
__android_log_print(blevel_to_android(bridge_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 (level == DEBUG_LEVEL_ERROR) udp_log_flush();
if (g_on_log) g_on_log(bridge_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_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) {
}
int utun_bridge_send_message(const char* channel_id, const char* text, const uint8_t reply_to[32], uint64_t request_id) {
if (!channel_id || !text) { bridge_log(BLEV_ERROR, "send: invalid arguments"); return -1; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "send: core stopped"); return -1; }
size_t datalen = strlen(text);
struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + datalen);
struct posted_task* post = u_calloc(1, sizeof(*post));
if (!req || !post) { bridge_log(BLEV_ERROR, "send: allocation failed"); u_free(req); u_free(post); return -1; }
req->inst = instance_lite_get_instance();
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;
memcpy(req->reply_to, reply_to, sizeof(req->reply_to));
req->request_id = request_id;
post->callback = chat_core_submit_trampoline; post->arg = req;
uasync_post_reserved(ua, post);
bridge_log(BLEV_INFO, "send queued ch=%s bytes=%zu request=%llu", channel_id, datalen, (unsigned long long)request_id);
return 0;
}
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 : "");
if (!name || !name[0]) { bridge_log(BLEV_ERROR, "create_channel: empty name"); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "create_channel: uasync not running"); return; }
struct chat_create_auto_arg* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "create_channel: OOM"); return; }
req->inst = instance_lite_get_instance();
snprintf(req->name, sizeof(req->name), "%s", name);
uasync_post(ua, chat_core_create_channel_auto_trampoline, req);
}
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"
#include "chat/chat_core_priv.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, uint64_t join_key) {
uint64_t real_node_id = sc_derive_node_id_from_pubkey(pubkey_bin);
if (real_node_id != node_id)
bridge_log(BLEV_WARN, "join: node_id Kotlin=0x%016llx != C=0x%016llx — using C value",
(unsigned long long)node_id, (unsigned long long)real_node_id);
bridge_log(BLEV_INFO, "join ch=%llu node=0x%016llx addrs=%d len=%d",
(unsigned long long)channel_id, (unsigned long long)real_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;
}
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) {
bridge_log(BLEV_ERROR, "join: instance not running");
utun_bridge_event(6, "{\"node_id\":0,\"result\":-7}"); /* CHAT_EVT_CONNECT_RESULT */
return;
}
chat_sync_connect_from_invite(inst, channel_id, real_node_id, pubkey_bin, addrs_data, addr_count, addrs_data_len, NULL, join_key);
}
void utun_bridge_request_invite_candidates(const char* channel_id) {
if (!channel_id || !channel_id[0]) {
bridge_log(BLEV_ERROR, "request_invite_candidates: null ch_id");
return;
}
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "request_invite_candidates: uasync not running"); return; }
struct chat_invite_cand_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "request_invite_candidates: OOM"); return; }
req->inst = instance_lite_get_instance();
snprintf(req->ch_id, sizeof(req->ch_id), "%s", channel_id);
bridge_log(BLEV_INFO, "request_invite_candidates ch=%s", channel_id);
uasync_post(ua, chat_invite_candidates_trampoline, req);
}
void utun_bridge_request_invite_link(const char* channel_id, uint64_t target_node_id, uint64_t request_id) {
if (!channel_id || !channel_id[0]) {
bridge_log(BLEV_ERROR, "request_invite_link: null ch_id");
return;
}
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "request_invite_link: uasync not running"); return; }
struct chat_invite_build_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "request_invite_link: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->request_id = request_id;
req->target_node_id = target_node_id;
bridge_log(BLEV_INFO, "request_invite_link ch=%s target=0x%016llx",
channel_id, (unsigned long long)target_node_id);
uasync_post(ua, chat_invite_build_link_trampoline, req);
}
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 (!uasync_is_running(ua)) return;
struct chat_create_auto_arg* req = u_calloc(1, sizeof(*req));
if (!req) return;
req->inst = instance_lite_get_instance();
snprintf(req->name, sizeof(req->name), "%s", channel_id);
bridge_log(BLEV_INFO, "connect_channel ch=%s", channel_id);
uasync_post(ua, chat_core_connect_channel_trampoline, req);
}
void utun_bridge_connect_member_node(const char* channel_id, uint64_t node_id) {
if (!channel_id || !channel_id[0] || node_id == 0) {
bridge_log(BLEV_ERROR, "connect_member_node: invalid args ch=%s node=0x%016llx",
channel_id ? channel_id : "(null)", (unsigned long long)node_id);
return;
}
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) return;
struct chat_connect_node_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "connect_member_node: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->node_id = node_id;
bridge_log(BLEV_INFO, "connect_member_node ch=%s node=0x%016llx", channel_id, (unsigned long long)node_id);
uasync_post(ua, chat_core_connect_node_trampoline, req);
}
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, const char* channel_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) {
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); return; }
}
sqlite3_finalize(st);
}
if (!channel_id || !channel_id[0]) return;
char peers_tbl[80], sql[256];
peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
snprintf(sql, sizeof(sql), "SELECT userinfo FROM \"%s\" WHERE node_id=?", peers_tbl);
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) return;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* ui = (const char*)sqlite3_column_text(st, 0);
if (ui && ui[0]) {
const char* ns = strstr(ui, "\"name\":\"");
if (ns) { ns += 8; const char* ne = strchr(ns, '"'); if (ne && ne > ns) { size_t nl = (size_t)(ne - ns); if (nl >= sz) nl = sz - 1; memcpy(out, ns, nl); out[nl] = '\0'; } }
}
}
sqlite3_finalize(st);
}
/* Формирует человекочитаемое тело превью последнего сообщения по content_type и "d"-полю. */
static void bridge_format_preview_body(const char* ct, const char* d, char* out, size_t out_sz) {
int is_text = (!ct || !ct[0] || strcmp(ct, "text/plain") == 0 || strcmp(ct, "voice_transcription") == 0);
if (is_text) {
size_t tl = strlen(d);
if (tl > 60) tl = 60;
if (tl >= out_sz) tl = out_sz - 1;
memcpy(out, d, tl); out[tl] = '\0';
return;
}
if (strcmp(ct, "audio/opus") == 0) {
int dur_ms = 0;
if (d[0] == '*') {
const char* pipe = strchr(d, '|');
size_t b64len = pipe ? (size_t)(pipe - d - 1) : strlen(d) - 1;
uint8_t dec[256]; size_t declen = b64_decode(d + 1, b64len, dec, sizeof(dec));
if (declen > 0) {
char raw[256]; if (declen >= sizeof(raw)) declen = sizeof(raw) - 1;
memcpy(raw, dec, declen); raw[declen] = '\0';
const char* dur = strstr(raw, ";dur=");
if (dur) dur_ms = atoi(dur + 5);
}
}
if (dur_ms > 0) {
int s = dur_ms / 1000;
snprintf(out, out_sz, "Voice message (%d:%02d)", s / 60, s % 60);
} else {
snprintf(out, out_sz, "Voice message");
}
return;
}
if (strncmp(ct, "video/", 6) == 0) { snprintf(out, out_sz, "Video"); return; }
if (strncmp(ct, "image/", 6) == 0) { snprintf(out, out_sz, "Photo"); return; }
/* файл и прочие нетекстовые: декодируем base64-префикс в имя файла */
if (d[0] == '*') {
const char* pipe = strchr(d, '|');
size_t b64len = pipe ? (size_t)(pipe - d - 1) : strlen(d) - 1;
uint8_t dec[512]; size_t declen = b64_decode(d + 1, b64len, dec, sizeof(dec));
if (declen > 0) {
if (declen >= out_sz) declen = out_sz - 1;
memcpy(out, dec, declen); out[declen] = '\0';
return;
}
}
snprintf(out, out_sz, "File");
}
char* utun_bridge_get_channels_json(void) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !db) {
bridge_log(BLEV_WARN, "get_channels: not initialized db=%p initialized=%d",
(void*)db, chat_core_is_initialized(inst));
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[160];
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 body[160] = "";
if (dptr && dlen > 0) {
const char* dp = (const char*)dptr;
char ct[32] = "text/plain";
const char* cts = strstr(dp, "\"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'; } } }
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 > 511) tl = 511; char d[512]; memcpy(d, ds, tl); d[tl] = '\0'; bridge_format_preview_body(ct, d, body, sizeof(body)); } }
}
char author_name[128];
bridge_get_node_name(db, (uint64_t)author, ch_id, 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, body);
}
sqlite3_finalize(lm);
}
int online = 0;
uint64_t my_id = inst->node_id;
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, const uint8_t* anchor, int exact) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance(); sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !db || !channel_id || !channel_id[0]) return u_strdup("[]");
char table[80], sql[2048], selection[1024]; msg_table_name(channel_id, table, sizeof(table));
if (exact && anchor) snprintf(selection, sizeof(selection), "SELECT * FROM \"%s\" WHERE message_id=?2", table);
else if (anchor) snprintf(selection, sizeof(selection),
"SELECT * FROM \"%s\" ORDER BY timestamp DESC,author_signature DESC LIMIT ?1 OFFSET min("
"max(0,(SELECT count(*) FROM \"%s\" n JOIN \"%s\" a ON a.message_id=?2 "
"WHERE (n.timestamp,n.author_signature)>(a.timestamp,a.author_signature)) - ?1/2),"
"max(0,(SELECT count(*) FROM \"%s\")-?1))", table, table, table, table);
else snprintf(selection, sizeof(selection), "SELECT * FROM \"%s\" ORDER BY timestamp DESC,author_signature DESC LIMIT ?1", table);
snprintf(sql, sizeof(sql),
"SELECT id,node_id,timestamp,local_attrs,message_id,"
"CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.ct') END,"
"CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.d') END,"
"CASE WHEN json_valid(CAST(data AS TEXT)) THEN json_extract(CAST(data AS TEXT),'$.reply_to') END"
" FROM (%s) ORDER BY timestamp,author_signature", selection);
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: messages query ch=%s: %s", channel_id, sqlite3_errmsg(db)); return u_strdup("[]");
}
sqlite3_bind_int(st, 1, limit > 0 ? limit : 50);
if (anchor) sqlite3_bind_blob(st, 2, anchor, 32, SQLITE_STATIC);
struct strbuf out = strbuf_new(); int step, result = strbuf_addf(&out, "[");
unsigned count = 0;
while (result >= 0 && (step = sqlite3_step(st)) == SQLITE_ROW) {
int64_t local_id = sqlite3_column_int64(st, 0), node_id = sqlite3_column_int64(st, 1), ts = sqlite3_column_int64(st, 2);
const char* attrs = (const char*)sqlite3_column_text(st, 3);
const uint8_t* id = sqlite3_column_blob(st, 4);
const char* type = (const char*)sqlite3_column_text(st, 5), *body = (const char*)sqlite3_column_text(st, 6);
if (sqlite3_column_bytes(st, 4) != 32 || sqlite3_column_bytes(st, 6) > DB_SYNC_MESSAGE_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: corrupt message ch=%s id=%lld", channel_id, (long long)local_id); result = -1; break;
}
char message_id[65], text[DB_SYNC_MESSAGE_MAX + 1];
for (unsigned i = 0; i < 32; i++) snprintf(message_id + 2 * i, 3, "%02x", id[i]);
snprintf(text, sizeof(text), "%s", body ? body : ""); if (!type) type = "text/plain";
/* У вложений '*' кодирует отображаемое имя; poll использует отдельный b64: формат. */
if (strcmp(type, "text/plain") && text[0] == '*') {
char* pipe = strchr(text, '|');
if (pipe) {
uint8_t decoded[1024]; size_t bytes = b64_decode(text + 1, pipe - text - 1, decoded, sizeof(decoded));
size_t suffix = strlen(pipe);
if (bytes && bytes + suffix < sizeof(text)) { memmove(text + bytes, pipe, suffix + 1); memcpy(text, decoded, bytes); }
}
}
char author[128], file_path[1024] = "";
bridge_get_node_name(db, (uint64_t)node_id, channel_id, author, sizeof(author));
if (!author[0]) snprintf(author, sizeof(author), "0x%016llx", (unsigned long long)node_id);
if (attrs) {
sqlite3_stmt* file = NULL;
if (sqlite3_prepare_v2(db, "SELECT CASE WHEN json_valid(?1) THEN json_extract(?1,'$.fp') END", -1, &file, NULL) == SQLITE_OK) {
sqlite3_bind_text(file, 1, attrs, -1, SQLITE_STATIC);
if (sqlite3_step(file) == SQLITE_ROW && sqlite3_column_type(file, 0) == SQLITE_TEXT) {
int bytes = snprintf(file_path, sizeof(file_path), "%s/media/%s/%s",
g_db_path, channel_id, sqlite3_column_text(file, 0));
if (bytes >= (int)sizeof(file_path)) {
file_path[0] = 0; DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "bridge: media path too long ch=%s", channel_id);
}
}
sqlite3_finalize(file);
} else DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: local attributes query: %s", sqlite3_errmsg(db));
}
result = strbuf_addf(&out, "%s{\"id\":%lld,\"messageId\":\"%s\",\"authorNodeId\":%lld,\"author\":",
count++ ? "," : "", (long long)local_id, message_id, (long long)node_id);
if (result >= 0) result = strbuf_add_json_string(&out, author);
if (result >= 0) result = strbuf_addf(&out, ",\"ts\":%lld,\"isOutgoing\":%s,\"text\":",
(long long)ts, node_id == (int64_t)inst->node_id ? "true" : "false");
if (result >= 0) result = strbuf_add_json_string(&out, text);
if (result >= 0) result = strbuf_addf(&out, ",\"contentType\":");
if (result >= 0) result = strbuf_add_json_string(&out, type);
if (result >= 0) result = strbuf_addf(&out, ",\"filePath\":");
if (result >= 0) result = strbuf_add_json_string(&out, file_path);
if (result >= 0) result = strbuf_addf(&out, ",\"localAttrs\":");
if (result >= 0) result = strbuf_add_json_string(&out, attrs);
if (result >= 0) result = strbuf_addf(&out, ",\"replyTo\":");
if (result >= 0) result = strbuf_add_json_string(&out, (const char*)sqlite3_column_text(st, 7));
if (result >= 0) result = strbuf_addf(&out, "}");
}
sqlite3_finalize(st);
if (result < 0 || step != SQLITE_DONE || strbuf_addf(&out, "]") < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: messages snapshot failed ch=%s: %s", channel_id, sqlite3_errmsg(db));
strbuf_free(&out); return u_strdup("[]");
}
return strbuf_detach(&out);
}
uint64_t utun_bridge_get_my_node_id(void) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
return (inst && chat_core_is_initialized(inst)) ? inst->node_id : 0;
}
/* ── DM (личные сообщения) ── */
void utun_bridge_dm_start(uint64_t node_id, const char* source_ch_id, const char* peer_name) {
if (!node_id) { bridge_log(BLEV_ERROR, "dm_start: invalid node_id=0"); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "dm_start: uasync not running"); return; }
struct dm_start_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "dm_start: OOM"); return; }
req->inst = instance_lite_get_instance();
req->peer_node_id = node_id;
if (source_ch_id) strncpy(req->source_ch_id, source_ch_id, sizeof(req->source_ch_id) - 1);
if (peer_name) strncpy(req->peer_name, peer_name, sizeof(req->peer_name) - 1);
bridge_log(BLEV_INFO, "dm_start queued peer=0x%016llx ch=%s",
(unsigned long long)node_id, source_ch_id ? source_ch_id : "");
uasync_post(ua, dm_start_trampoline, req);
}
void utun_bridge_dm_send(const char* conv_id, const char* text) {
if (!conv_id || !conv_id[0] || !text) { bridge_log(BLEV_ERROR, "dm_send: invalid args"); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "dm_send: uasync not running"); return; }
size_t len = strlen(text);
struct dm_send_req* req = u_calloc(1, sizeof(struct dm_send_req) + len);
if (!req) { bridge_log(BLEV_ERROR, "dm_send: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->conv_id, conv_id, sizeof(req->conv_id) - 1);
req->data_len = (uint32_t)len;
req->data = (uint8_t*)(req + 1);
memcpy(req->data, text, len);
bridge_log(BLEV_DEBUG, "dm_send queued conv=%s len=%zu", conv_id, len);
uasync_post(ua, dm_send_trampoline, req);
}
void utun_bridge_dm_request_conversations(void) {
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_WARN, "DM conversations: core stopped"); return; }
struct dm_conversations_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "DM conversations: OOM"); return; }
req->inst = instance_lite_get_instance();
uasync_post(ua, dm_conversations_trampoline, req);
}
void utun_bridge_dm_request_messages(const char* conv_id) {
struct UASYNC* ua = instance_lite_get_uasync();
if (!conv_id || !conv_id[0] || !uasync_is_running(ua)) { bridge_log(BLEV_WARN, "DM history: invalid request/core stopped"); return; }
struct dm_messages_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "DM history: OOM"); return; }
req->inst = instance_lite_get_instance();
snprintf(req->conv_id, sizeof(req->conv_id), "%s", conv_id);
uasync_post(ua, dm_messages_trampoline, req);
}
int utun_bridge_dm_send_file(const char* conv_id, const char* path, const char* name) {
struct UASYNC* ua = instance_lite_get_uasync();
if (!conv_id || !path || !name || !uasync_is_running(ua)) { bridge_log(BLEV_WARN, "DM file: invalid request/core stopped"); return -1; }
struct dm_file_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "DM file: OOM"); return -1; }
if (strlen(conv_id) >= sizeof(req->conv) || strlen(path) >= sizeof(req->path) || strlen(name) >= sizeof(req->info.name)) {
bridge_log(BLEV_ERROR, "DM file: path, conversation or name too long"); u_free(req); return -1;
}
req->inst = instance_lite_get_instance();
snprintf(req->conv, sizeof(req->conv), "%s", conv_id);
snprintf(req->path, sizeof(req->path), "%s", path);
req->info.kind = ATTACHMENT_FILE;
req->remove_source = 1; /* PM picker передаёт собственный уникальный cache-файл. */
snprintf(req->info.name, sizeof(req->info.name), "%s", name);
if (attachment_validate(&req->info)) { bridge_log(BLEV_ERROR, "DM file: invalid name"); u_free(req); return -1; }
struct posted_task* post = u_calloc(1, sizeof(*post));
if (!post) { bridge_log(BLEV_ERROR, "DM file: completion allocation failed"); u_free(req); return -1; }
post->callback = dm_send_file_trampoline; post->arg = req;
uasync_post_reserved(ua, post);
return 0;
}
void utun_bridge_set_debug_level(const char* category, const char* level) {
if (!level) { bridge_log(BLEV_ERROR, "Debug level: missing level"); return; }
if (!category || !category[0]) {
debug_level_t lvl = debug_level_from_name(level);
debug_set_level(lvl);
} else {
debug_set_category_level_by_name(category, level);
}
bridge_log(BLEV_INFO, "Debug level applied: %s=%s", category && category[0] ? category : "global", level);
}
void utun_bridge_chat_setting_set(const char* name, const char* value) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) { bridge_log(BLEV_ERROR, "chat_setting_set: instance not running"); return; }
chat_core_set_setting(inst, name, value);
}
int utun_bridge_chat_setting_get_int(const char* name, int def) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
return chat_setting_get_int(inst, name, def);
}
/* ──────────────────────────────────────────────────────────────────
* Channel members (queried from shared DB)
* ────────────────────────────────────────────────────────────────── */
#include "../../../src/routing_layer/topo_node_sqlite.h"
#include "../../../lib/json_flat.h"
#include <openssl/evp.h>
char* utun_bridge_get_channel_members_json(const char* channel_id) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !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.name,''),"
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
" COALESCE(p.node_type,0), COALESCE(p.node_RTT,0), COALESCE(p.storage,0),"
" COALESCE(p.userinfo,''),p.source,length(p.adm_tags_sig),COALESCE(p.deleted,0),p.radio_muted"
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" ORDER BY 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 = inst->node_id;
int first = 1;
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t node_id = (uint64_t)sqlite3_column_int64(st, 0);
const char* name = (const char*)sqlite3_column_text(st, 1);
const char* local_nick = (const char*)sqlite3_column_text(st, 2);
const char* adm_tags = (const char*)sqlite3_column_text(st, 3);
int node_type = sqlite3_column_int(st, 4);
int node_rtt = sqlite3_column_int(st, 5);
int storage_flag = sqlite3_column_int(st, 6);
int is_admin = 0, is_super = 0, is_moder = 0, is_verified = 0, is_deleted = sqlite3_column_int(st, 10);
int storage_role = 0;
if (adm_tags) {
char buf[256];
if (json_flat_get(adm_tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_super = 1;
if (json_flat_get(adm_tags, "admin", buf, sizeof(buf)) == 0) {
const char* last = buf; for (const char* p = buf; *p; p++) if (*p == 'e' || *p == 'd') last = p;
if (*last == 'e') is_admin = 1;
}
if (json_flat_get(adm_tags, "moder", buf, sizeof(buf)) == 0) {
const char* last = buf; for (const char* p = buf; *p; p++) if (*p == 'e' || *p == 'd') last = p;
if (*last == 'e') is_moder = 1;
}
if (json_flat_get(adm_tags, "verified", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_verified = 1;
if (json_flat_get(adm_tags, "deleted", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_deleted = 1;
if (!is_deleted && sqlite3_column_int(st, 8) == 0 && sqlite3_column_int(st, 9) == 64 &&
json_flat_get(adm_tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) storage_role = 1;
}
char* esc_name = json_escape_alloc(local_nick && local_nick[0] ? local_nick :
(name && name[0] ? name : ""));
char* esc_userinfo = json_escape_alloc((const char*)sqlite3_column_text(st, 7));
if (!esc_name || !esc_userinfo) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "Android members: JSON allocation failed channel=%s node=%016llx",
channel_id, (unsigned long long)node_id);
u_free(esc_name); u_free(esc_userinfo); continue;
}
const char* sep = first ? "" : ",";
first = 0;
size_t needed = snprintf(NULL, 0,
"%s{\"n\":\"%s\",\"ni\":%llu,\"on\":0,\"ad\":%d,\"sp\":%d,\"md\":%d,\"st\":%d,"
"\"vd\":%d,\"dl\":%d,\"tp\":%d,\"rt\":%d,\"is\":%d,\"ui\":\"%s\",\"sr\":%d,\"rm\":%d}",
sep, esc_name, (unsigned long long)node_id,
is_admin, is_super, is_moder, storage_flag,
is_verified, is_deleted, node_type, node_rtt, (node_id == my_id) ? 1 : 0, esc_userinfo, storage_role, sqlite3_column_int(st, 11));
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\":0,\"ad\":%d,\"sp\":%d,\"md\":%d,\"st\":%d,"
"\"vd\":%d,\"dl\":%d,\"tp\":%d,\"rt\":%d,\"is\":%d,\"ui\":\"%s\",\"sr\":%d,\"rm\":%d}",
sep, esc_name, (unsigned long long)node_id,
is_admin, is_super, is_moder, storage_flag,
is_verified, is_deleted, node_type, node_rtt, (node_id == my_id) ? 1 : 0, esc_userinfo, storage_role, sqlite3_column_int(st, 11));
u_free(esc_userinfo);
u_free(esc_name);
}
sqlite3_finalize(st);
json[pos++] = ']'; json[pos] = '\0';
return json;
}
/* Не продолжаем сборку JSON после усечения строки: pos должен оставаться внутри буфера. */
static int member_detail_append(char* buf, size_t size, size_t* pos, const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
int written = vsnprintf(buf + *pos, size - *pos, fmt, ap);
va_end(ap);
if (written < 0 || (size_t)written >= size - *pos) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "member detail: JSON append failed used=%zu capacity=%zu required=%d",
*pos, size, written);
return -1;
}
*pos += (size_t)written;
return 0;
}
char* utun_bridge_get_member_detail_json(const char* channel_id, uint64_t node_id) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !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(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);
int64_t last_seen = sqlite3_column_int64(st, 1);
int64_t created = sqlite3_column_int64(st, 2);
char* esc_name = json_escape_alloc(name && name[0] ? name : "");
int rc = member_detail_append(buf, sizeof(buf), &pos,
",\"n\":\"%s\",\"on\":0,\"ls\":%lld,\"cr\":%lld",
esc_name, (long long)last_seen, (long long)created);
u_free(esc_name);
if (rc < 0) { sqlite3_finalize(st); return u_strdup("{}"); }
}
sqlite3_finalize(st);
}
char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl));
/* Родитель дерева приглашений относится к каналу; без подписи это ещё неизвестная связь. */
char sql[512];
snprintf(sql, sizeof(sql),
"SELECT p.signed_by, length(p.signature), p.source, COALESCE(NULLIF(i.local_nick,''),n.name,'')"
" FROM \"%s\" p LEFT JOIN \"%s\" i ON i.node_id=p.signed_by"
" LEFT JOIN nodes n ON n.node_id=p.signed_by WHERE p.node_id=?", peers_tbl, peers_tbl);
int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc == SQLITE_ROW) {
int known = sqlite3_column_type(st, 0) != SQLITE_NULL && sqlite3_column_int(st, 1) == 64 &&
sqlite3_column_int(st, 2) == 0;
uint64_t inviter_id = (uint64_t)sqlite3_column_int64(st, 0);
const char* inviter_name = known && inviter_id ? (const char*)sqlite3_column_text(st, 3) : "";
char* escaped = json_escape_alloc(inviter_name);
if (!escaped) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "member invitation: name allocation failed ch=%s node=%016llx",
channel_id, (unsigned long long)node_id);
sqlite3_finalize(st); return u_strdup("{}");
}
char inviter_json[32] = "null";
if (known) snprintf(inviter_json, sizeof(inviter_json), "%llu", (unsigned long long)inviter_id);
int append_rc = member_detail_append(buf, sizeof(buf), &pos,
",\"inviter_id\":%s,\"inviter_name\":\"%s\"", inviter_json, escaped);
u_free(escaped);
if (append_rc < 0) { sqlite3_finalize(st); return u_strdup("{}"); }
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "member invitation: ch=%s node=%016llx known=%d inviter=%016llx name=%s",
channel_id, (unsigned long long)node_id, known, (unsigned long long)inviter_id, inviter_name);
} else {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "member invitation: read failed ch=%s node=%016llx rc=%d: %s",
channel_id, (unsigned long long)node_id, rc, sqlite3_errmsg(db));
}
sqlite3_finalize(st); st = NULL;
if (member_detail_append(buf, sizeof(buf), &pos, ",\"adr\":[") < 0) return u_strdup("{}");
if (sqlite3_prepare_v2(db,
"SELECT family, protocol, address, port, socket_id, addr_type, COALESCE(rtt,0)"
" FROM node_addresses WHERE node_id=? ORDER BY socket_id, addr_type, 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 at = sqlite3_column_int(st, 5);
int rtt = sqlite3_column_int(st, 6);
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) {
char tmp[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, addr, tmp, sizeof(tmp));
snprintf(addr_str, sizeof(addr_str), "%s", tmp);
}
}
const char* pn;
if (proto & 0x04) pn = "TCP+REALITY";
else switch (proto) { case 1: pn = "UDP"; break; case 2: pn = "TCP"; break; default: pn = "UDP/TCP"; break; }
if (member_detail_append(buf, sizeof(buf), &pos,
"%s{\"fm\":%d,\"pt\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"si\":%d,\"at\":%d,\"rt\":%d}",
af ? "" : ",", family, pn, addr_str[0] ? addr_str : "?", port, sid, at, rtt) < 0) {
sqlite3_finalize(st); return u_strdup("{}");
}
af = 0;
}
sqlite3_finalize(st);
}
if (member_detail_append(buf, sizeof(buf), &pos, "]}") < 0) return u_strdup("{}");
return u_strdup(buf);
}
int utun_bridge_toggle_member_radio_mute(const char* channel_id, uint64_t node_id) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
if (!inst || !uasync_is_running(ua) || !channel_id || !channel_id[0] || strlen(channel_id) >= 64 || !node_id) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: Android mute request rejected: core unavailable or invalid member"); return -1;
}
struct chat_member_radio_mute_req* req = u_calloc(1, sizeof(*req));
struct posted_task* post = u_calloc(1, sizeof(*post));
if (!req || !post) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: Android mute request allocation failed"); u_free(req); u_free(post); return -1;
}
req->inst = inst; req->node_id = node_id; strcpy(req->ch_id, channel_id);
post->callback = chat_core_toggle_member_radio_mute_trampoline; post->arg = req;
uasync_post_reserved(ua, post);
return 0;
}
void utun_bridge_set_member_nick(const char* channel_id, uint64_t node_id, const char* nick) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !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)");
}
struct member_tags_edit_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
uint64_t node_id;
int admin, supernode, moder, storage, verified, deleted;
};
static void member_tags_edit_trampoline(void* arg) {
struct member_tags_edit_req* req = (struct member_tags_edit_req*)arg;
if (!req || !req->inst) { u_free(arg); return; }
struct chat_member_tags* t = chat_member_tags_load(req->inst, req->ch_id, req->node_id);
if (!t) {
bridge_log(BLEV_ERROR, "set_member_flags: tags_load failed ch=%s nid=0x%016llx",
req->ch_id, (unsigned long long)req->node_id);
u_free(arg);
return;
}
chat_member_tags_set(t, "admin", req->admin ? "yes" : "no");
chat_member_tags_set(t, "moder", req->moder ? "yes" : "no");
chat_member_tags_set(t, "supernode", req->supernode ? "yes" : "no");
chat_member_tags_set(t, "storage", req->storage ? "yes" : "no");
chat_member_tags_set(t, "verified", req->verified ? "yes" : "no");
chat_member_tags_set(t, "deleted", req->deleted ? "yes" : "no");
int rc = chat_member_tags_commit(req->inst, t);
bridge_log(rc == 0 ? BLEV_INFO : BLEV_ERROR,
"set_member_flags ch=%s nid=0x%016llx admin=%d super=%d moder=%d storage=%d verified=%d deleted=%d rc=%d",
req->ch_id, (unsigned long long)req->node_id, req->admin, req->supernode,
req->moder, req->storage, req->verified, req->deleted, rc);
chat_member_tags_free(t);
u_free(arg);
}
int utun_bridge_is_channel_owner(const char* channel_id) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
sqlite3* db = chat_core_get_db(inst);
if (!chat_core_is_initialized(inst) || !db || !channel_id || !channel_id[0]) return 0;
uint8_t priv[32];
return topo_node_sqlite_channel_get_priv(db, channel_id, priv) == 0 ? 1 : 0;
}
void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id,
int admin, int supernode, int moder, int storage_flag,
int verified, int deleted) {
if (!channel_id || node_id == 0) { bridge_log(BLEV_ERROR, "set_member_flags: invalid args"); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "set_member_flags: uasync not running"); return; }
struct member_tags_edit_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "set_member_flags: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->node_id = node_id;
req->admin = admin; req->supernode = supernode; req->moder = moder;
req->storage = storage_flag; req->verified = verified; req->deleted = deleted;
uasync_post(ua, member_tags_edit_trampoline, req);
bridge_log(BLEV_DEBUG, "set_member_flags queued ch=%s nid=0x%016llx admin=%d super=%d moder=%d storage=%d verified=%d deleted=%d",
channel_id, (unsigned long long)node_id, admin, supernode, moder, storage_flag, verified, deleted);
}
char* utun_bridge_get_member_links_json(uint64_t node_id) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
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;
char la[INET6_ADDRSTRLEN] = ""; int lf = 0; uint16_t lp = 0;
if (lk->conn) {
const struct sockaddr_storage* lsa = lk->conn->interface_addr.ss_family ? &lk->conn->interface_addr : &lk->conn->local_addr;
if (lsa->ss_family == AF_INET) {
inet_ntop(AF_INET, &((struct sockaddr_in*)lsa)->sin_addr, la, sizeof(la)); lf = 4; lp = ntohs(((struct sockaddr_in*)lsa)->sin_port);
} else if (lsa->ss_family == AF_INET6) {
inet_ntop(AF_INET6, &((struct sockaddr_in6*)lsa)->sin6_addr, la, sizeof(la)); lf = 6; lp = ntohs(((struct sockaddr_in6*)lsa)->sin6_port);
}
} else if (lk->is_tcp && lk->tcp_link) {
const struct sockaddr_storage* lsa = stcp_link_get_local_addr(lk->tcp_link);
if (lsa && lsa->ss_family == AF_INET) {
inet_ntop(AF_INET, &((struct sockaddr_in*)lsa)->sin_addr, la, sizeof(la)); lf = 4; lp = ntohs(((struct sockaddr_in*)lsa)->sin_port);
} else if (lsa && lsa->ss_family == AF_INET6) {
inet_ntop(AF_INET6, &((struct sockaddr_in6*)lsa)->sin6_addr, la, sizeof(la)); lf = 6; lp = ntohs(((struct sockaddr_in6*)lsa)->sin6_port);
}
}
char ra[INET6_ADDRSTRLEN] = ""; int rf = 0; uint16_t rp = 0;
int st = lk->link_state, ls = lk->link_status;
if (lk->is_tcp && lk->tcp_link && stcp_link_is_ready(lk->tcp_link)) { st = 3; ls = 1; }
const struct sockaddr_storage* rsa = &lk->remote_addr;
if (rsa->ss_family == AF_INET) {
inet_ntop(AF_INET, &((struct sockaddr_in*)rsa)->sin_addr, ra, sizeof(ra)); rf = 4; rp = ntohs(((struct sockaddr_in*)rsa)->sin_port);
} else if (rsa->ss_family == AF_INET6) {
inet_ntop(AF_INET6, &((struct sockaddr_in6*)rsa)->sin6_addr, ra, sizeof(ra)); rf = 6; rp = ntohs(((struct sockaddr_in6*)rsa)->sin6_port);
}
const char* sep = first ? "" : ","; first = 0;
const char* sn = lk->conn ? lk->conn->name : "";
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos,
"%s{\"id\":%d,\"pt\":\"%s\",\"lf\":%d,\"la\":\"%s\",\"lp\":%d,\"rf\":%d,\"ra\":\"%s\",\"rp\":%d,\"st\":%d,\"ls\":%d,\"is\":%d,\"sn\":\"%s\"}",
sep, lk->local_link_id, lk->reality_set ? "TCP+REALITY" : (lk->is_tcp ? "TCP" : "UDP"), lf, la, (int)lp, rf, ra, (int)rp, st, ls, lk->is_server, sn);
}
if (pos < (int)sizeof(buf))
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "]}");
bridge_log(BLEV_INFO, "getMemberLinks node=0x%016llx → %s", (unsigned long long)node_id, buf);
return u_strdup(buf);
}
/* ──────────────────────────────────────────────────────────────────
* Передача прав админа (канальный приватный ключ) другому узлу
* ────────────────────────────────────────────────────────────────── */
void utun_bridge_transfer_admin(const char* channel_id, uint64_t node_id) {
if (!channel_id || node_id == 0) { bridge_log(BLEV_ERROR, "transfer_admin: invalid args"); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "transfer_admin: uasync not running"); return; }
struct chat_transfer_admin_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "transfer_admin: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->target_node_id = node_id;
uasync_post(ua, chat_core_transfer_admin_trampoline, req);
bridge_log(BLEV_INFO, "transfer_admin queued ch=%s → 0x%016llx", channel_id, (unsigned long long)node_id);
}
/* ──────────────────────────────────────────────────────────────────
* Удаление канала (локальное)
* ────────────────────────────────────────────────────────────────── */
void utun_bridge_delete_channel(const char* channel_id) {
if (!channel_id || !channel_id[0]) { bridge_log(BLEV_ERROR, "delete_channel: empty ch_id"); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "delete_channel: uasync not running"); return; }
struct chat_delete_channel_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "delete_channel: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
uasync_post(ua, chat_core_delete_channel_trampoline, req);
bridge_log(BLEV_INFO, "delete_channel queued ch=%s", channel_id);
}
/* ── Per-channel «play sound on message» ── */
struct sound_setting_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
int on;
};
static void sound_setting_trampoline(void* arg) {
struct sound_setting_req* req = (struct sound_setting_req*)arg;
if (!req || !req->inst) { u_free(arg); return; }
chat_core_set_sound_on_message(req->inst, req->ch_id, req->on);
u_free(arg);
}
int utun_bridge_get_sound_on_message(const char* channel_id) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst || !chat_core_is_initialized(inst) || !channel_id || !channel_id[0]) return 1;
return chat_core_get_sound_on_message(inst, channel_id);
}
void utun_bridge_set_sound_on_message(const char* channel_id, int on) {
if (!channel_id || !channel_id[0]) { bridge_log(BLEV_ERROR, "set_sound: empty ch_id"); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "set_sound: uasync not running"); return; }
struct sound_setting_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "set_sound: OOM"); return; }
req->inst = instance_lite_get_instance();
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->on = on ? 1 : 0;
uasync_post(ua, sound_setting_trampoline, req);
bridge_log(BLEV_INFO, "set_sound queued ch=%s on=%d", channel_id, on ? 1 : 0);
}
/* ──────────────────────────────────────────────────────────────────
* Local sockets state (for auto-mode UI display)
* ────────────────────────────────────────────────────────────────── */
#include "../../../lib/socket_compat.h"
char* utun_bridge_get_local_sockets_json(void) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst || !chat_core_is_initialized(inst)) return u_strdup("[]");
size_t cap = 4096, pos = 1;
char* json = u_malloc(cap);
if (!json) return u_strdup("[]");
json[0] = '[';
int first = 1;
bridge_log(BLEV_INFO, "getLocalSockets: etcp_sockets=%p", (void*)inst->etcp_sockets);
/* Helper: count per-socket links_up / links_down */
#define SOCK_COUNT_LINKS(_s, _lu, _ld) do { \
int *_p_lu = &(_lu), *_p_ld = &(_ld); \
*_p_lu = 0; *_p_ld = 0; \
if (inst->connections) { \
for (struct ll_entry* _e = inst->connections->head; _e; _e = _e->next) { \
struct conn_queue_entry* _ce = (struct conn_queue_entry*)_e->data; \
if (!_ce || !_ce->conn) continue; \
for (struct ETCP_LINK* _lk = _ce->conn->links; _lk; _lk = _lk->next) { \
if (_lk->conn != (_s)) continue; \
if (_lk->link_state == 3) (*_p_lu)++; else (*_p_ld)++; \
} \
} \
} \
if (inst->tcp_connections) { \
for (struct ll_entry* _e = inst->tcp_connections->head; _e; _e = _e->next) { \
struct tcp_conn_entry* _te = (struct tcp_conn_entry*)_e->data; \
if (!_te || !_te->etcp_conn) continue; \
for (struct ETCP_LINK* _lk = _te->etcp_conn->links; _lk; _lk = _lk->next) { \
if (_lk->conn != (_s)) continue; \
if (_lk->link_state == 3) (*_p_lu)++; else (*_p_ld)++; \
} \
} \
} \
} while(0)
/* TCP sockets */
{ struct ETCP_SOCKET* s = inst->etcp_sockets;
while (s) {
if (!s->is_tcp) { s = s->next; continue; }
char ip_str[INET6_ADDRSTRLEN] = ""; int port = 0;
const struct sockaddr_storage* addr = s->interface_addr.ss_family ? &s->interface_addr : &s->local_addr;
if (addr->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)addr;
port = ntohs(sin->sin_port);
inet_ntop(AF_INET, &sin->sin_addr, ip_str, sizeof(ip_str));
} else if (addr->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
port = ntohs(sin6->sin6_port);
inet_ntop(AF_INET6, &sin6->sin6_addr, ip_str, sizeof(ip_str));
}
int lu, ld; SOCK_COUNT_LINKS(s, lu, ld);
int state = 2; /* bound and listening */
const char* sep = first ? "" : ","; first = 0;
const char* pn = s->reality_enabled ? "TCP+REALITY" : "TCP";
size_t needed = snprintf(NULL, 0,
"%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"%s\",\"st\":%d,\"lu\":%d,\"ld\":%d}",
sep, s->sock_id, s->name, ip_str, port, pn, state, lu, ld);
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{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"%s\",\"st\":%d,\"lu\":%d,\"ld\":%d}",
sep, s->sock_id, s->name, ip_str, port, pn, state, lu, ld);
s = s->next;
}
}
/* UDP sockets */
{ struct ETCP_SOCKET* es = inst->etcp_sockets;
while (es) {
if (es->is_tcp) { es = es->next; continue; }
char ip_str[INET6_ADDRSTRLEN] = ""; int port = 0;
const struct sockaddr_storage* addr = es->interface_addr.ss_family ? &es->interface_addr : &es->local_addr;
if (addr->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)addr;
port = ntohs(sin->sin_port);
inet_ntop(AF_INET, &sin->sin_addr, ip_str, sizeof(ip_str));
} else if (addr->ss_family == AF_INET6) {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
port = ntohs(sin6->sin6_port);
inet_ntop(AF_INET6, &sin6->sin6_addr, ip_str, sizeof(ip_str));
}
int lu, ld; SOCK_COUNT_LINKS(es, lu, ld);
int state = 2; /* bound and listening */
const char* sep = first ? "" : ","; first = 0;
size_t needed = snprintf(NULL, 0,
"%s{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"UDP\",\"st\":%d,\"lu\":%d,\"ld\":%d}",
sep, es->sock_id, es->name, ip_str, port, state, lu, ld);
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{\"si\":%u,\"nm\":\"%s\",\"ad\":\"%s\",\"po\":%d,\"pt\":\"UDP\",\"st\":%d,\"lu\":%d,\"ld\":%d}",
sep, es->sock_id, es->name, ip_str, port, state, lu, ld);
es = es->next;
}
}
#undef SOCK_COUNT_LINKS
json[pos++] = ']'; json[pos] = '\0';
return json;
}
void utun_bridge_on_network_change(void) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) return;
auto_socket_on_network_change(inst);
bridge_log(BLEV_DEBUG, "network change notified to auto_socket");
}
static void bridge_set_active_trampoline(void* arg) {
int active = (int)(intptr_t)arg;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) return;
if (utun_set_client_activity(inst, active) < 0) {
bridge_log(BLEV_ERROR, "client_activity transition failed requested=%s", active ? "ACTIVE" : "STANDBY");
return;
}
bridge_log(BLEV_DEBUG, "client_activity set to %s", active ? "ACTIVE" : "STANDBY");
}
void utun_bridge_set_active(int active) {
struct UASYNC* ua = instance_lite_get_uasync();
if (!ua) return;
uasync_post(ua, bridge_set_active_trampoline, (void*)(intptr_t)active);
}
static void bridge_notify_network_active_trampoline(void* arg) {
(void)arg;
bridge_log(BLEV_DEBUG, "network activity notified to standby");
standby_notify_network_activity();
}
void utun_bridge_notify_network_active(void) {
struct UASYNC* ua = instance_lite_get_uasync();
if (!ua) return;
uasync_post(ua, bridge_notify_network_active_trampoline, NULL);
}
void utun_bridge_collect_conn_list(void) {
instance_lite_collect_conn_list();
}
void utun_bridge_collect_conn_metrics(uint64_t peer_node_id) {
instance_lite_collect_conn_metrics(peer_node_id);
}
void utun_bridge_restart(const char* config_text) {
bridge_log(BLEV_INFO, "bridge restart: posting to instance");
#ifdef __ANDROID__
{
void instance_event_handler(int type, const uint8_t* data, int len);
instance_lite_set_event_handler(instance_event_handler);
}
#endif
instance_lite_restart(config_text);
/* Wait for instance to be ready before returning to Kotlin.
Normal restart (live): is_running() true immediately → no delay.
Dead restart (after stop): polls until g_running=1 → blocks until init done. */
for (int i = 0; i < 500 && !instance_lite_is_running(); i++)
usleep(10000);
if (!instance_lite_is_running())
bridge_log(BLEV_ERROR, "bridge restart: instance not ready after 5s");
}
void utun_bridge_update_sockets(const char* config_text) {
bridge_log(BLEV_INFO, "bridge update_sockets: posting to instance");
instance_lite_update_sockets(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, const uint8_t reply_to[32], uint64_t request_id) {
voice_recorder_init(g_db_path); /* idempotent: гарантирует g_rec после stop/restart */
return voice_recorder_start(channel_id, 48000, 1, reply_to, request_id);
}
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,
const uint8_t reply_to[32], uint64_t request_id) {
return attachment_send(channel_id, file_path, db_path, reply_to, request_id);
}
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 (!uasync_is_running(ua)) return -1;
struct attachment_dl_req* req = u_calloc(1, sizeof(*req));
if (!req) return -1;
req->inst = instance_lite_get_instance();
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;
}
int utun_bridge_video_send(const char* channel_id, const char* file_path, const char* db_path,
int duration_ms, int width, int height, const uint8_t reply_to[32], uint64_t request_id) {
return video_send(channel_id, file_path, db_path, duration_ms, width, height, reply_to, request_id);
}
int utun_bridge_photo_send(const char* channel_id, const char* file_path, const char* db_path,
int width, int height, const uint8_t reply_to[32], uint64_t request_id) {
return photo_send(channel_id, file_path, db_path, width, height, reply_to, request_id);
}
/* ──────────────────────────────────────────────────────────────────
* 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 || fread(&ch, 2, 1, f) != 1 || fread(&fs, 2, 1, f) != 1 ||
(sr != 8000 && sr != 12000 && sr != 16000 && sr != 24000 && sr != 48000) ||
(ch != 1 && ch != 2) || fs != sr / 50) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: invalid header rate=%u channels=%u frame=%u", sr, ch, fs);
fclose(f); return NULL;
}
opus_codec_decoder_t* dec = opus_codec_decoder_create((int)sr, (int)ch);
if (!dec) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: decoder allocation failed"); fclose(f); return NULL; }
struct voice_decode_handle* h = u_calloc(1, sizeof(*h));
if (!h) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: handle allocation failed"); 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 = 0;
if (fread(&plen, 2, 1, f) != 1) goto invalid_stream;
if (!plen) {
if (!h->total_samples || fgetc(f) != EOF || ferror(f)) goto invalid_stream;
break;
}
if (plen > 4000 || h->total_samples > INT_MAX - (int)fs * ch || fseek(f, plen, SEEK_CUR)) goto invalid_stream;
h->total_samples += (int)fs * (int)ch;
}
if (fseek(f, pos, SEEK_SET)) goto invalid_stream;
*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;
invalid_stream:
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: truncated/invalid stream path=%s samples=%d", file_path, h->total_samples);
utun_bridge_voice_decode_close(h); return NULL;
}
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 || plen > 4000 || fread(packet, 1, plen, h->file) != plen) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: stream changed after validation"); return -1;
}
int decoded = opus_codec_decode(h->dec, packet, plen, buf + written, h->frame_samples);
if (decoded != h->frame_samples) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice decode: invalid packet result=%d", decoded); return -1; }
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");
}
/* ──────────────────────────────────────────────────────────────────
* P2P аудио-звонок (сигналинг)
* ────────────────────────────────────────────────────────────────── */
void utun_bridge_call_start(const char* ch_id, uint64_t peer_node_id, uint64_t call_id) {
if (!ch_id || !ch_id[0] || peer_node_id == 0 || call_id == 0) {
bridge_log(BLEV_ERROR, "call_start: bad args ch=%s peer=0x%llx id=0x%llx",
ch_id ? ch_id : "(null)",
(unsigned long long)peer_node_id, (unsigned long long)call_id);
return;
}
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "call_start: uasync not running"); return; }
struct call_start_arg* a = (struct call_start_arg*)u_calloc(1, sizeof(*a));
if (!a) { bridge_log(BLEV_ERROR, "call_start: OOM"); return; }
a->inst = instance_lite_get_instance();
snprintf(a->ch_id, sizeof(a->ch_id), "%s", ch_id);
a->peer_node_id = peer_node_id;
a->call_id = call_id;
bridge_log(BLEV_INFO, "call_start ch=%s peer=0x%llx id=0x%llx",
ch_id, (unsigned long long)peer_node_id, (unsigned long long)call_id);
uasync_post(ua, call_start_trampoline, a);
}
static void bridge_call_id_action(uint64_t call_id, const char* name,
void (*trampoline)(void*)) {
if (call_id == 0) { bridge_log(BLEV_ERROR, "call_%s: bad call_id=0", name); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "call_%s: uasync not running", name); return; }
struct call_id_arg* a = (struct call_id_arg*)u_calloc(1, sizeof(*a));
if (!a) { bridge_log(BLEV_ERROR, "call_%s: OOM", name); return; }
a->inst = instance_lite_get_instance();
a->call_id = call_id;
bridge_log(BLEV_INFO, "call_%s id=0x%llx", name, (unsigned long long)call_id);
uasync_post(ua, trampoline, a);
}
void utun_bridge_call_accept(uint64_t call_id) { bridge_call_id_action(call_id, "accept", call_accept_trampoline); }
void utun_bridge_call_decline(uint64_t call_id) { bridge_call_id_action(call_id, "decline", call_decline_trampoline); }
void utun_bridge_call_hangup(uint64_t call_id) { bridge_call_id_action(call_id, "hangup", call_hangup_trampoline); }
int utun_bridge_call_is_online(const char* ch_id, uint64_t peer_node_id) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) return 0;
return call_is_online(inst, ch_id, peer_node_id);
}
/* ──────────────────────────────────────────────────────────────────
* P2P аудио-звонок (медиа: call_audio.c)
* ────────────────────────────────────────────────────────────────── */
int utun_bridge_call_audio_start(uint64_t call_id) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_audio_start: no instance");
return -1;
}
if (call_audio_init(inst) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call_audio_start: call_audio_init failed");
return -1;
}
return call_audio_start(call_id);
}
int utun_bridge_call_audio_feed(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us) {
return call_audio_feed_pcm(call_id, pcm, count, capture_pts_us);
}
void utun_bridge_call_audio_stop(void) {
call_audio_stop();
}
int utun_bridge_call_audio_pull(uint64_t call_id, int16_t* out, int max_samples) {
return call_audio_pull_pcm(call_id, out, max_samples);
}
void utun_bridge_call_audio_set_aec(int enabled) {
call_audio_set_aec_enabled(enabled);
}
void utun_bridge_call_audio_set_aec_delay(int frames) {
call_audio_set_aec_delay_frames(frames);
}
/* ──────────────────────────────────────────────────────────────────
* Рация (PTT walkie-talkie): подписка + аудио-движок
* ────────────────────────────────────────────────────────────────── */
void utun_bridge_radio_set_active(uint64_t group_id, int on) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
if (!inst || !ua) { bridge_log(BLEV_ERROR, "radio_set_active: no instance/ua"); return; }
struct radio_set_active_arg* a = (struct radio_set_active_arg*)u_calloc(1, sizeof(*a));
if (!a) return;
a->inst = inst;
a->group_id = group_id;
a->on = on;
uasync_post(ua, radio_set_active_trampoline, a);
}
int utun_bridge_radio_subscribers(uint64_t group_id) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) return 0;
return radio_subscribers(inst, group_id);
}
int utun_bridge_radio_audio_start(uint64_t group_id, int capture_channels) {
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio_audio_start: no instance"); return -1; }
if (radio_audio_init(inst) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio_audio_start: radio_audio_init failed"); return -1; }
return radio_audio_start(group_id, capture_channels);
}
int utun_bridge_radio_audio_feed(uint64_t group_id, const int16_t* pcm, int count) {
return radio_audio_feed_pcm(group_id, pcm, count);
}
void utun_bridge_radio_audio_stop(void) {
radio_audio_stop();
}
int utun_bridge_radio_audio_pull(uint64_t group_id, int16_t* out, int max_samples) {
return radio_audio_pull_pcm(group_id, out, max_samples);
}
int utun_bridge_radio_auto_mode(void) {
return radio_audio_auto_mode();
}
void utun_bridge_radio_talk_begin(uint64_t group_id) {
radio_audio_talk_begin(group_id);
}
void utun_bridge_radio_talk_end(uint64_t group_id) {
radio_audio_talk_end(group_id);
}
/* ──────────────────────────────────────────────────────────────────
* 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) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: g_jvm=NULL"); return; }
if (!g_event_callback) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: g_event_callback=NULL"); return; }
if (!g_event_mid) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: g_event_mid=NULL"); 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) {
bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: AttachCurrentThread failed");
return;
}
attached = 1;
} else if (res != JNI_OK) {
bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: GetEnv failed res=%d", (int)res);
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);
}
void instance_event_handler(int type, const uint8_t* data, int len) {
jni_on_event(type, data, len);
}
/* ── JNI call-audio: PCM отдаётся через nativeCallAudioPull (см. ниже) ── */
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, jlong joinKey,
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, (uint64_t)joinKey);
(*env)->ReleaseByteArrayElements(env, addrsData, (jbyte*)ad, JNI_ABORT);
(*env)->ReleaseByteArrayElements(env, pubkey, (jbyte*)pk, JNI_ABORT);
}
/* ── nativeDeriveNodeId ── */
JNIEXPORT jlong JNICALL Java_com_utun_chat_data_NativeLib_nativeDeriveNodeId(
JNIEnv* env, jobject thiz, jbyteArray pubkey) {
(void)thiz;
if (!pubkey) return 0;
jsize pk_len = (*env)->GetArrayLength(env, pubkey);
if (pk_len < 32) return 0;
uint8_t* pk = (uint8_t*)(*env)->GetByteArrayElements(env, pubkey, NULL);
uint64_t node_id = sc_derive_node_id_from_pubkey(pk);
(*env)->ReleaseByteArrayElements(env, pubkey, (jbyte*)pk, JNI_ABORT);
return (jlong)node_id;
}
/* ── nativeRequestInviteCandidates / nativeRequestInviteLink ── */
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestInviteCandidates(
JNIEnv* env, jobject thiz, jstring channelId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_request_invite_candidates(ch);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestInviteLink(
JNIEnv* env, jobject thiz, jstring channelId, jlong targetNodeId, jlong requestId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_request_invite_link(ch, (uint64_t)targetNodeId, (uint64_t)requestId);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
}
/* ── 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;
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; }
}
/* Пустой массив означает отсутствие ссылки; иначе принимаем только полный глобальный ID. */
static int jni_message_id(JNIEnv* env, jbyteArray value, uint8_t id[32]) {
memset(id, 0, 32);
int length = value ? (*env)->GetArrayLength(env, value) : -1;
if (length != 0 && length != 32) { bridge_log(BLEV_ERROR, "message ID: invalid byte length=%d", length); return -1; }
if (length) (*env)->GetByteArrayRegion(env, value, 0, 32, (jbyte*)id);
if ((*env)->ExceptionCheck(env)) { bridge_log(BLEV_ERROR, "message ID: JNI copy failed"); return -1; }
return 0;
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeSendMessage(
JNIEnv* env, jobject thiz, jstring channelId, jbyteArray text, jbyteArray replyTo, jlong requestId) {
(void)thiz;
uint8_t reply[32];
if (jni_message_id(env, replyTo, reply) < 0 || !text) return JNI_FALSE;
jsize length = (*env)->GetArrayLength(env, text);
if (length <= 0 || length > DB_SYNC_MESSAGE_MAX) { bridge_log(BLEV_ERROR, "send: invalid text bytes=%d", length); return JNI_FALSE; }
char tx[DB_SYNC_MESSAGE_MAX + 1];
(*env)->GetByteArrayRegion(env, text, 0, length, (jbyte*)tx); tx[length] = 0;
if ((*env)->ExceptionCheck(env)) { bridge_log(BLEV_ERROR, "send: text copy failed"); return JNI_FALSE; }
if (memchr(tx, 0, (size_t)length)) { bridge_log(BLEV_ERROR, "send: embedded NUL in text"); return JNI_FALSE; }
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
int rc = utun_bridge_send_message(ch, tx, reply, (uint64_t)requestId);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
return rc == 0 ? JNI_TRUE : JNI_FALSE;
}
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, jbyteArray anchor, jboolean exact) {
(void)thiz;
uint8_t id[32];
if (jni_message_id(env, anchor, id) < 0) return NULL;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
char* json = utun_bridge_get_messages_json(ch, (int)limit, (*env)->GetArrayLength(env, anchor) ? id : NULL, exact);
/* String(byte[], UTF-8) корректно восстанавливает четырёхбайтовые emoji. */
jclass strings = (*env)->FindClass(env, "java/lang/String");
jmethodID constructor = strings ? (*env)->GetMethodID(env, strings, "<init>", "([BLjava/lang/String;)V") : NULL;
size_t bytes = json ? strlen(json) : 0;
jbyteArray data = (*env)->NewByteArray(env, (jsize)bytes);
jstring charset = (*env)->NewStringUTF(env, "UTF-8");
jstring result = NULL;
if (constructor && data && charset) {
(*env)->SetByteArrayRegion(env, data, 0, (jsize)bytes, (const jbyte*)json);
if (!(*env)->ExceptionCheck(env)) result = (jstring)(*env)->NewObject(env, strings, constructor, data, charset);
}
if (!result) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: UTF-8 message result allocation failed");
if (data) (*env)->DeleteLocalRef(env, data);
if (charset) (*env)->DeleteLocalRef(env, charset);
if (strings) (*env)->DeleteLocalRef(env, strings);
u_free(json);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
return result;
}
/* UTF-8 JSON приходит байтами, чтобы emoji не превращались в JNI modified UTF-8. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFeedbackCommand(
JNIEnv* env, jobject thiz, jbyteArray json, jlong request_id) {
(void)thiz;
struct UASYNC* ua = instance_lite_get_uasync();
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!json || !inst || !uasync_is_running(ua)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: feedback core unavailable"); return JNI_FALSE;
}
jsize len = (*env)->GetArrayLength(env, json);
if (len <= 0 || len > 16384) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: feedback command size=%d", len); return JNI_FALSE;
}
struct chat_feedback_request* req = u_malloc(sizeof(*req) + len + 1);
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: feedback allocation failed"); return JNI_FALSE; }
req->inst = inst; req->request_id = (uint64_t)request_id;
(*env)->GetByteArrayRegion(env, json, 0, len, (jbyte*)(req + 1));
if ((*env)->ExceptionCheck(env)) { u_free(req); DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "bridge: feedback JNI copy failed"); return JNI_FALSE; }
((char*)(req + 1))[len] = 0; uasync_post(ua, chat_feedback_command_trampoline, req); return JNI_TRUE;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmStart(
JNIEnv* env, jobject thiz, jlong nodeId, jstring sourceChId, jstring peerName) {
(void)thiz;
const char* ch = sourceChId ? (*env)->GetStringUTFChars(env, sourceChId, NULL) : NULL;
const char* nm = peerName ? (*env)->GetStringUTFChars(env, peerName, NULL) : NULL;
utun_bridge_dm_start((uint64_t)nodeId, ch, nm);
if (nm) (*env)->ReleaseStringUTFChars(env, peerName, nm);
if (ch) (*env)->ReleaseStringUTFChars(env, sourceChId, ch);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmSend(
JNIEnv* env, jobject thiz, jstring convId, jstring text) {
(void)thiz;
const char* c = (*env)->GetStringUTFChars(env, convId, NULL);
const char* t = (*env)->GetStringUTFChars(env, text, NULL);
utun_bridge_dm_send(c, t);
if (t) (*env)->ReleaseStringUTFChars(env, text, t);
if (c) (*env)->ReleaseStringUTFChars(env, convId, c);
}
/* UI получает подтверждение commit через CHAT_EVT_DM_INVITE_RESULT, а не через return JNI. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeDmInvite(
JNIEnv* env, jobject thiz, jlong request_id, jlong peer, jstring source, jlong channel, jstring name, jboolean cancel) {
(void)thiz;
struct UASYNC* ua = instance_lite_get_uasync();
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!uasync_is_running(ua) || !inst) { bridge_log(BLEV_ERROR, "invite PM: core unavailable"); return JNI_FALSE; }
struct dm_invite_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "invite PM: request allocation failed"); return JNI_FALSE; }
const char* src = (*env)->GetStringUTFChars(env, source, NULL);
const char* nm = src ? (*env)->GetStringUTFChars(env, name, NULL) : NULL;
if (!src || !nm) {
if (src) (*env)->ReleaseStringUTFChars(env, source, src);
bridge_log(BLEV_ERROR, "invite PM: string conversion failed"); u_free(req); return JNI_FALSE;
}
req->inst = inst; req->request_id = (uint64_t)request_id; req->peer_node_id = (uint64_t)peer;
req->channel_id = (uint64_t)channel; req->cancel = cancel ? 1 : 0;
snprintf(req->source_ch_id, sizeof(req->source_ch_id), "%s", src);
snprintf(req->peer_name, sizeof(req->peer_name), "%s", nm);
(*env)->ReleaseStringUTFChars(env, source, src); (*env)->ReleaseStringUTFChars(env, name, nm);
uasync_post(ua, dm_invite_trampoline, req);
return JNI_TRUE;
}
/* Чтение членства для выбора групп и открытия уже присоединённого канала. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeIsChannelMember(
JNIEnv* env, jobject thiz, jstring channel, jlong node) {
(void)thiz;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
const char* id = (*env)->GetStringUTFChars(env, channel, NULL);
if (!id) { bridge_log(BLEV_ERROR, "channel membership: string conversion failed"); return JNI_FALSE; }
int member = inst && topo_node_sqlite_member_in_channel(inst->topo_sqlite_db, id, (uint64_t)node);
(*env)->ReleaseStringUTFChars(env, channel, id);
return member ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmRequestConversations(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
utun_bridge_dm_request_conversations();
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmRequestCallRoute(
JNIEnv* env, jobject thiz, jstring convId) {
(void)thiz;
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_WARN, "PM call route: core stopped"); return; }
const char* conv = (*env)->GetStringUTFChars(env, convId, NULL);
if (!conv) return;
struct dm_messages_req* req = u_calloc(1, sizeof(*req));
if (req) {
req->inst = instance_lite_get_instance();
snprintf(req->conv_id, sizeof(req->conv_id), "%s", conv);
uasync_post(ua, dm_call_route_trampoline, req);
} else bridge_log(BLEV_ERROR, "PM call route: allocation failed");
(*env)->ReleaseStringUTFChars(env, convId, conv);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDmRequestMessages(
JNIEnv* env, jobject thiz, jstring convId) {
(void)thiz;
const char* c = (*env)->GetStringUTFChars(env, convId, NULL);
utun_bridge_dm_request_messages(c);
if (c) (*env)->ReleaseStringUTFChars(env, convId, c);
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeDmSendFile(
JNIEnv* env, jobject thiz, jstring convId, jstring path, jstring name) {
(void)thiz;
const char* c = (*env)->GetStringUTFChars(env, convId, NULL);
const char* p = (*env)->GetStringUTFChars(env, path, NULL);
const char* n = (*env)->GetStringUTFChars(env, name, NULL);
int rc = utun_bridge_dm_send_file(c, p, n);
if (n) (*env)->ReleaseStringUTFChars(env, name, n);
if (p) (*env)->ReleaseStringUTFChars(env, path, p);
if (c) (*env)->ReleaseStringUTFChars(env, convId, c);
return rc == 0 ? JNI_TRUE : JNI_FALSE;
}
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_nativeUpdateSockets(
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_update_sockets(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 jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeIsRunning(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return instance_lite_is_running() ? 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();
}
/* GUI запрашивает готовый снимок асинхронно, CPU-часы читает только поток ядра. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestCpuLoad(JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
struct UASYNC* ua = instance_lite_get_uasync();
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (!ua || !inst || !instance_lite_is_running()) return JNI_FALSE;
struct posted_task* task = u_calloc(1, sizeof(*task));
if (!task) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "CPU load request allocation failed"); return JNI_FALSE; }
task->callback = chat_core_collect_cpu_load_trampoline; task->arg = inst;
uasync_post_reserved(ua, task);
return JNI_TRUE;
}
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_nativeConnectMemberNode(
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_connect_member_node(ch, (uint64_t)nodeId);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
}
JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetDebugCategories(JNIEnv* env, jobject thiz) {
(void)thiz;
struct strbuf names = strbuf_new();
for (int category = 1; category < DEBUG_CATEGORY_COUNT; category++) {
const char* name = debug_get_category_name(category);
if (strcmp(name, "UNKNOWN") == 0) continue;
if (strbuf_addf(&names, "%s%s", names.len ? "," : "", name) < 0) {
bridge_log(BLEV_ERROR, "Debug categories: allocation failed");
strbuf_free(&names); return NULL;
}
}
jstring result = (*env)->NewStringUTF(env, strbuf_str(&names));
if (!result) bridge_log(BLEV_ERROR, "Debug categories: Java string allocation failed");
strbuf_free(&names); return result;
}
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);
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
if (inst) chat_core_set_setting(inst, 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);
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
int val = chat_setting_get_int(inst, n, (int)def);
if (n) (*env)->ReleaseStringUTFChars(env, name, n);
return (jint)val;
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeGetSoundOnMessage(
JNIEnv* env, jobject thiz, jstring chId) {
(void)thiz;
const char* c = (*env)->GetStringUTFChars(env, chId, NULL);
int on = utun_bridge_get_sound_on_message(c);
if (c) (*env)->ReleaseStringUTFChars(env, chId, c);
return (jint)on;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetSoundOnMessage(
JNIEnv* env, jobject thiz, jstring chId, jboolean on) {
(void)thiz;
const char* c = (*env)->GetStringUTFChars(env, chId, NULL);
utun_bridge_set_sound_on_message(c, on ? 1 : 0);
if (c) (*env)->ReleaseStringUTFChars(env, chId, 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;
}
/* ── Voice recording JNI ── */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceStart(
JNIEnv* env, jobject thiz, jstring channelId, jbyteArray replyTo, jlong requestId) {
(void)thiz;
uint8_t reply[32];
if (jni_message_id(env, replyTo, reply) < 0) return JNI_FALSE;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
int rc = utun_bridge_voice_start(ch, reply, (uint64_t)requestId);
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;
return utun_bridge_voice_stop(&duration_ms) == 0 ? (jint)duration_ms : -1;
}
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, jbyteArray replyTo, jlong requestId) {
(void)thiz;
uint8_t reply[32];
if (jni_message_id(env, replyTo, reply) < 0) return JNI_FALSE;
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, reply, (uint64_t)requestId);
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;
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeVideoSend(
JNIEnv* env, jobject thiz, jstring channelId, jstring filePath,
jint durationMs, jint width, jint height, jbyteArray replyTo, jlong requestId) {
(void)thiz;
uint8_t reply[32];
if (jni_message_id(env, replyTo, reply) < 0) return JNI_FALSE;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
const char* fp = (*env)->GetStringUTFChars(env, filePath, NULL);
int rc = utun_bridge_video_send(ch, fp, g_db_path, (int)durationMs, (int)width, (int)height, reply, (uint64_t)requestId);
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_nativePhotoSend(
JNIEnv* env, jobject thiz, jstring channelId, jstring filePath,
jint width, jint height, jbyteArray replyTo, jlong requestId) {
(void)thiz;
uint8_t reply[32];
if (jni_message_id(env, replyTo, reply) < 0) return JNI_FALSE;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
const char* fp = (*env)->GetStringUTFChars(env, filePath, NULL);
int rc = utun_bridge_photo_send(ch, fp, g_db_path, (int)width, (int)height, reply, (uint64_t)requestId);
if (fp) (*env)->ReleaseStringUTFChars(env, filePath, fp);
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 ── */
/* Живой снимок группы читается в uasync; результат приходит как MEMBER_LIST. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestMemberList(
JNIEnv* env, jobject thiz, jstring channelId) {
(void)thiz;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
if (!inst || !ua || !channelId) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: missing instance/uasync/channel"); return JNI_FALSE;
}
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
if (!ch) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: cannot read channel"); return JNI_FALSE; }
struct chat_member_list_req* req = u_calloc(1, sizeof(*req));
if (!req || !ch[0] || strlen(ch) >= sizeof(req->ch_id)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: allocation/invalid channel ch=%s", ch);
u_free(req); (*env)->ReleaseStringUTFChars(env, channelId, ch); return JNI_FALSE;
}
req->inst = inst;
memcpy(req->ch_id, ch, strlen(ch) + 1);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: queued ch=%s", ch);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
uasync_post(ua, chat_core_request_member_list_trampoline, req);
return JNI_TRUE;
}
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 jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeToggleMemberRadioMute(
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId) {
(void)thiz;
if (!channelId) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: null Android mute channel"); return JNI_FALSE; }
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
if (!ch) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: Android mute channel conversion failed"); return JNI_FALSE; }
int rc = utun_bridge_toggle_member_radio_mute(ch, (uint64_t)nodeId);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
return rc == 0 ? JNI_TRUE : JNI_FALSE;
}
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 jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeIsChannelOwner(
JNIEnv* env, jobject thiz, jstring channelId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
int owner = utun_bridge_is_channel_owner(ch);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
return (jboolean)owner;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeTransferAdmin(
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_transfer_admin(ch, (uint64_t)nodeId);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDeleteChannel(
JNIEnv* env, jobject thiz, jstring channelId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_delete_channel(ch);
(*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 moder, jboolean storage,
jboolean verified, jboolean deleted) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_set_member_flags(ch, (uint64_t)nodeId, admin, supernode, moder, storage, verified, deleted);
(*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;
}
/* ── Local sockets JSON ── */
JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetLocalSockets(
JNIEnv* env, jobject thiz) {
(void)thiz;
char* json = utun_bridge_get_local_sockets_json();
jstring result = (*env)->NewStringUTF(env, json ? json : "[]");
u_free(json);
return result;
}
/* ── Network change notification ── */
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeOnNetworkChanged(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
utun_bridge_on_network_change();
}
/* ── App foreground/background (Android lifecycle) → client_activity ── */
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetActive(
JNIEnv* env, jobject thiz, jboolean active) {
(void)env; (void)thiz;
utun_bridge_set_active(active ? 1 : 0);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeNotifyNetworkActive(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
utun_bridge_notify_network_active();
}
/* ── Connection monitor ── */
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCollectConnList(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
utun_bridge_collect_conn_list();
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCollectConnMetrics(
JNIEnv* env, jobject thiz, jlong peerNodeId) {
(void)env; (void)thiz;
utun_bridge_collect_conn_metrics((uint64_t)peerNodeId);
}
/* ── P2P аудио-звонок (сигналинг) ── */
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallStart(
JNIEnv* env, jobject thiz, jstring channelId, jlong peerNodeId, jlong callId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_call_start(ch, (uint64_t)peerNodeId, (uint64_t)callId);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAccept(
JNIEnv* env, jobject thiz, jlong callId) {
(void)env; (void)thiz;
utun_bridge_call_accept((uint64_t)callId);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallDecline(
JNIEnv* env, jobject thiz, jlong callId) {
(void)env; (void)thiz;
utun_bridge_call_decline((uint64_t)callId);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallHangup(
JNIEnv* env, jobject thiz, jlong callId) {
(void)env; (void)thiz;
utun_bridge_call_hangup((uint64_t)callId);
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeCallIsOnline(
JNIEnv* env, jobject thiz, jstring channelId, jlong peerNodeId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
int online = utun_bridge_call_is_online(ch, (uint64_t)peerNodeId);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
return online ? JNI_TRUE : JNI_FALSE;
}
/* ── P2P аудио-звонок (медиа) ── */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioStart(
JNIEnv* env, jobject thiz, jlong callId) {
(void)env; (void)thiz;
return (utun_bridge_call_audio_start((uint64_t)callId) == 0) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioFeed(
JNIEnv* env, jobject thiz, jlong callId, jshortArray samples, jlong capturePtsUs) {
(void)thiz;
if (!samples) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call capture: null PCM");
return JNI_FALSE;
}
jsize len = (*env)->GetArrayLength(env, samples);
jshort* data = (*env)->GetShortArrayElements(env, samples, NULL);
if (!data) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call capture: JNI PCM access failed");
return JNI_FALSE;
}
int rc = utun_bridge_call_audio_feed((uint64_t)callId, (const int16_t*)data, (int)len, capturePtsUs);
(*env)->ReleaseShortArrayElements(env, samples, data, JNI_ABORT);
return (rc == 0) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioStop(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
utun_bridge_call_audio_stop();
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioProgress(
JNIEnv* env, jobject thiz, jlong callId, jlong epoch, jlong position, jlong atUs, jlong nowUs, jboolean measured) {
(void)env; (void)thiz;
call_audio_playback_progress(callId, epoch, position, atUs, nowUs, measured);
}
/* Предвыделенный снимок: epoch, source_epoch, played, written, pts, valid, measured, held_back_us. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioClock(
JNIEnv* env, jobject thiz, jlong callId, jlong nowUs, jlongArray out) {
(void)thiz;
if (!out || (*env)->GetArrayLength(env, out) != 8) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call clock: expected LongArray(8)");
return JNI_FALSE;
}
struct call_clock_snapshot s;
if (call_audio_get_clock(callId, nowUs, &s) != 0) return JNI_FALSE;
jlong values[8] = {s.epoch, s.source_epoch, s.played_samples, s.written_samples,
s.pts_us, s.valid, s.measured, s.held_back_us};
(*env)->SetLongArrayRegion(env, out, 0, 8, values);
if ((*env)->ExceptionCheck(env)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call clock: JNI snapshot copy failed");
return JNI_FALSE;
}
return JNI_TRUE;
}
/* Диагностика включается тем же фильтром call=debug, что и нативные сводки. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioDiagnostics(
JNIEnv* env, jobject thiz, jlong callId, jlongArray out) {
(void)thiz;
if (!debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_CALL)) return JNI_FALSE;
if (!out || (*env)->GetArrayLength(env, out) != 8) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call diagnostics: expected LongArray(8)");
return JNI_FALSE;
}
struct call_jitter_diagnostics d;
if (call_audio_get_diagnostics(callId, &d) != 0) return JNI_FALSE;
jlong values[8] = {d.encoded_ms, d.processing_ms, d.reserve_ms, d.arrival_spread_ms,
d.pcm_samples, d.tempo_x100, d.underruns, d.waiting};
(*env)->SetLongArrayRegion(env, out, 0, 8, values);
if ((*env)->ExceptionCheck(env)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call diagnostics: JNI snapshot copy failed");
return JNI_FALSE;
}
return JNI_TRUE;
}
/* ── nativeCallAudioPull(callId, out) → возвращает число сэмплов ──
* out — предвыделенный ShortArray (960), заполняется из аудио-потока Kotlin. */
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioPull(
JNIEnv* env, jobject thiz, jlong callId, jshortArray out) {
(void)thiz;
if (!out) return 0;
jsize len = (*env)->GetArrayLength(env, out);
jshort* data = (*env)->GetShortArrayElements(env, out, NULL);
if (!data) return 0;
int n = utun_bridge_call_audio_pull((uint64_t)callId, (int16_t*)data, (int)len);
(*env)->ReleaseShortArrayElements(env, out, data, 0);
return (jint)n;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioSetAec(
JNIEnv* env, jobject thiz, jboolean enabled) {
(void)env; (void)thiz;
utun_bridge_call_audio_set_aec(enabled ? 1 : 0);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCallAudioSetAecDelay(
JNIEnv* env, jobject thiz, jint frames) {
(void)env; (void)thiz;
utun_bridge_call_audio_set_aec_delay((int)frames);
}
/* ── Рация (PTT walkie-talkie) ── */
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioSetActive(
JNIEnv* env, jobject thiz, jlong groupId, jboolean on) {
(void)env; (void)thiz;
utun_bridge_radio_set_active((uint64_t)groupId, on ? 1 : 0);
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioSubscribers(
JNIEnv* env, jobject thiz, jlong groupId) {
(void)env; (void)thiz;
return (jint)utun_bridge_radio_subscribers((uint64_t)groupId);
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAudioStart(
JNIEnv* env, jobject thiz, jlong groupId, jint channels) {
(void)env; (void)thiz;
return (utun_bridge_radio_audio_start((uint64_t)groupId, (int)channels) == 0) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioCaptureStart(
JNIEnv* env, jobject thiz, jlong groupId, jint channels, jint mode) {
(void)env; (void)thiz;
return radio_audio_capture_start((uint64_t)groupId, (int)channels, (int)mode) == 0 ? JNI_TRUE : JNI_FALSE;
}
/* Снимок настроек применяется целиком в потоке ядра; callback разрешает новый capture. */
struct radio_config_request {
struct posted_task task;
struct UTUN_INSTANCE* inst;
jlong request_id;
jint values[7];
};
static void radio_config_applied(jlong request_id, int success) {
JNIEnv* env = NULL;
int attached = 0;
if (!g_jvm || !g_config_callback) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config callback unavailable request=%lld", (long long)request_id);
return;
}
jint rc = (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6);
if (rc == JNI_EDETACHED) {
if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != JNI_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config: attach JVM failed");
return;
}
attached = 1;
} else if (rc != JNI_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config: GetEnv failed rc=%d", rc);
return;
}
jclass cls = (*env)->GetObjectClass(env, g_config_callback);
jmethodID method = cls ? (*env)->GetMethodID(env, cls, "onRadioSettingsApplied", "(JZ)V") : NULL;
if (method) (*env)->CallVoidMethod(env, g_config_callback, method, request_id, success ? JNI_TRUE : JNI_FALSE);
if ((*env)->ExceptionCheck(env)) {
(*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env);
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config callback failed request=%lld", (long long)request_id);
}
if (cls) (*env)->DeleteLocalRef(env, cls);
if (attached) (*g_jvm)->DetachCurrentThread(g_jvm);
}
static void radio_config_apply(void* arg) {
struct radio_config_request* req = arg;
static const char* keys[] = {"radio_auto_enabled", "radio_auto_mode", "radio_level_on_dbfs", "radio_level_off_dbfs",
"radio_auto_hangover_ms", "radio_vad_threshold", "radio_vad_compressor_max_gain_db"};
int success = req->inst == instance_lite_get_instance();
if (success) {
struct chat_setting_state next = req->inst->chat_settings;
for (int i = 0; i < 7; ++i) {
char value[16]; snprintf(value, sizeof(value), "%d", req->values[i]);
if (chat_setting_set_state(&next, keys[i], value) != 0) success = 0;
}
if (req->values[3] >= req->values[2]) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config: invalid thresholds on=%d off=%d", req->values[2], req->values[3]);
success = 0;
}
if (success) req->inst->chat_settings = next;
} else DEBUG_WARN(DEBUG_CATEGORY_RADIO, "Android radio config: stale instance request=%lld", (long long)req->request_id);
DEBUG_INFO(DEBUG_CATEGORY_RADIO, "Android radio config request=%lld success=%d enabled=%d mode=%d on=%d off=%d hangover=%dms",
(long long)req->request_id, success, req->values[0], req->values[1], req->values[2], req->values[3], req->values[4]);
radio_config_applied(req->request_id, success);
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioConfigure(
JNIEnv* env, jobject thiz, jlong request_id, jintArray settings) {
(void)thiz;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
if (!inst || !uasync_is_running(ua) || !settings || (*env)->GetArrayLength(env, settings) != 7) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config: core unavailable or invalid snapshot request=%lld", (long long)request_id);
return JNI_FALSE;
}
struct radio_config_request* req = u_calloc(1, sizeof(*req));
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config allocation failed"); return JNI_FALSE; }
(*env)->GetIntArrayRegion(env, settings, 0, 7, req->values);
if ((*env)->ExceptionCheck(env)) {
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio config: cannot copy snapshot"); u_free(req); return JNI_FALSE;
}
req->inst = inst; req->request_id = request_id;
req->task.callback = radio_config_apply; req->task.arg = req;
uasync_post_reserved(ua, &req->task); // Очередь освобождает снимок, в том числе при shutdown.
return JNI_TRUE;
}
JNIEXPORT jfloatArray JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioInputStatus(JNIEnv* env, jobject thiz) {
(void)thiz;
struct radio_audio_input_status status = radio_audio_get_input_status();
jfloat values[] = {status.level_dbfs, (float)status.valid, (float)status.blocked, status.vad_probability, (float)status.vad_valid,
status.vad_gain_db, (float)status.vad_compressor_enabled};
jfloatArray result = (*env)->NewFloatArray(env, 7);
if (!result) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "Android radio input status allocation failed"); return NULL; }
(*env)->SetFloatArrayRegion(env, result, 0, 7, values);
return result;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioCaptureStop(JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
radio_audio_capture_stop();
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioCaptureDiscontinuity(
JNIEnv* env, jobject thiz, jlong groupId) {
(void)env; (void)thiz;
radio_audio_capture_discontinuity((uint64_t)groupId);
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAudioFeed(
JNIEnv* env, jobject thiz, jlong groupId, jshortArray samples) {
(void)thiz;
if (!samples) return JNI_FALSE;
jsize len = (*env)->GetArrayLength(env, samples);
jshort* data = (*env)->GetShortArrayElements(env, samples, NULL);
int rc = utun_bridge_radio_audio_feed((uint64_t)groupId, (const int16_t*)data, (int)len);
(*env)->ReleaseShortArrayElements(env, samples, data, JNI_ABORT);
return (rc == 0) ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAudioStop(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
utun_bridge_radio_audio_stop();
}
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioTransmitting(JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return radio_audio_transmitting() ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAutoMode(
JNIEnv* env, jobject thiz) {
(void)env; (void)thiz;
return (jint)utun_bridge_radio_auto_mode();
}
JNIEXPORT jint JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioAudioPull(
JNIEnv* env, jobject thiz, jlong groupId, jshortArray out, jlong playedFrames) {
(void)thiz;
if (!out) return 0;
jsize len = (*env)->GetArrayLength(env, out);
jshort* data = (*env)->GetShortArrayElements(env, out, NULL);
if (!data) return 0;
radio_audio_playback_progress((uint64_t)groupId, (uint64_t)playedFrames, 0);
int n = utun_bridge_radio_audio_pull((uint64_t)groupId, (int16_t*)data, (int)len);
(*env)->ReleaseShortArrayElements(env, out, data, 0);
return (jint)n;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioTalkBegin(
JNIEnv* env, jobject thiz, jlong groupId) {
(void)env; (void)thiz;
utun_bridge_radio_talk_begin((uint64_t)groupId);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeRadioTalkEnd(
JNIEnv* env, jobject thiz, jlong groupId) {
(void)env; (void)thiz;
utun_bridge_radio_talk_end((uint64_t)groupId);
}
/* Снимок читается в потоке ядра; локальное JNI-событие 53 не меняет сетевой протокол.
* id/group/version:8LE, hash:32, can_edit/failed:1, size:2LE, data:size. */
static void android_avatar_snapshot(void* arg) {
struct group_avatar_gui_request* req = arg;
struct group_avatar_record record;
memset(&record, 0, sizeof(record));
int failed = group_avatar_get(req->inst, req->group, &record) < 0;
char channel[32]; uint8_t key[32];
snprintf(channel, sizeof(channel), "%llu", (unsigned long long)req->group);
int can_edit = topo_node_sqlite_channel_get_priv(chat_core_get_db(req->inst), channel, key) == 0;
memset(key, 0, sizeof(key));
if (failed) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "Android avatar: snapshot failed group=%s id=%llu", channel,
(unsigned long long)req->id);
size_t size = failed ? 0 : record.size;
uint8_t* event = u_calloc(1, 60 + size);
if (!event) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "Android avatar: snapshot allocation failed id=%llu", (unsigned long long)req->id);
u_free(req); return;
}
mf_put64(event, req->id); mf_put64(event + 8, req->group); mf_put64(event + 16, record.version);
memcpy(event + 24, record.hash, 32); event[56] = can_edit; event[57] = failed;
event[58] = size & 255; event[59] = size >> 8;
if (size) memcpy(event + 60, record.data, size);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "Android avatar: snapshot group=%s id=%llu version=%llu size=%zu edit=%d failed=%d",
channel, (unsigned long long)req->id, (unsigned long long)record.version, size, can_edit, failed);
jni_on_event(53, event, (int)(60 + size));
u_free(event); u_free(req);
}
/* Только операции аватарок; файловые пути ограничены каталогом avatars. Владение req переходит uasync. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeAvatarOperation(
JNIEnv* env, jobject thiz, jint op, jlong id, jlong group, jlong server, jlong owner,
jstring remote, jstring local, jbyteArray data) {
(void)thiz;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
if (!inst || !ua || !uasync_is_running(ua) || !id || (!group && op != 7) || op < 0 || op > 8) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "Android avatar: unavailable/invalid operation=%d id=%lld group=%llx", op,
(long long)id, (unsigned long long)group); return JNI_FALSE;
}
const char* path = remote ? (*env)->GetStringUTFChars(env, remote, NULL) : NULL;
const char* file = local ? (*env)->GetStringUTFChars(env, local, NULL) : NULL;
void* req = NULL; uasync_post_callback_t callback = NULL;
if ((remote && !path) || (local && !file)) goto invalid;
if (((op >= 1 && op <= 4) || op == 8) && (!server || !owner || !path ||
(op == 3 || op == 8 ? strcmp(path, "avatars") != 0 :
strncmp(path, "avatars/", 8) != 0 || !mf_avatar_file_valid(path + 8)))) goto invalid;
if (op == 0 || op == 6) {
struct group_avatar_gui_request* r = u_calloc(1, sizeof(*r)); req = r;
if (!r) goto invalid;
r->inst = inst; r->id = id; r->group = group;
if (op == 6) {
jsize size = data ? (*env)->GetArrayLength(env, data) : 0;
if (size > GROUP_AVATAR_MAX) goto invalid;
r->size = size;
if (size) (*env)->GetByteArrayRegion(env, data, 0, size, (jbyte*)r->data);
if ((*env)->ExceptionCheck(env)) goto invalid;
}
callback = op == 0 ? android_avatar_snapshot : group_avatar_gui_set;
} else if (op == 1 || op == 2) {
if (!file || !file[0] || strlen(file) > MF_PATH_MAX) goto invalid;
struct mf_transfer_request* r = u_calloc(1, sizeof(*r)); req = r;
if (!r) goto invalid;
r->inst = inst; r->id = id; r->group = group; r->server = server; r->owner = owner;
uint8_t limit[8];
if (!data || (*env)->GetArrayLength(env, data) != 8) goto invalid;
(*env)->GetByteArrayRegion(env, data, 0, 8, (jbyte*)limit);
if ((*env)->ExceptionCheck(env)) goto invalid;
r->upload = op == 2; r->max_bytes = mf_get64(limit);
if (!r->max_bytes || r->max_bytes > mf_avatar_file_limit(path + 8)) goto invalid;
memcpy(r->remote, path, strlen(path) + 1); memcpy(r->local, file, strlen(file) + 1);
callback = member_files_gui_transfer;
} else if (op == 3 || op == 4 || op == 8) {
struct mf_gui_request* r = u_calloc(1, sizeof(*r)); req = r;
if (!r) goto invalid;
r->inst = inst; r->group = group; r->server = server;
r->message.kind = MF_REQUEST; r->message.command = op == 3 ? MF_MKDIR : op == 8 ? MF_LIST : MF_STAT;
r->message.request_id = id; r->message.owner = owner;
if (op == 8) {
uint8_t page[24];
if (!data || (*env)->GetArrayLength(env, data) != 24) goto invalid;
(*env)->GetByteArrayRegion(env, data, 0, 24, (jbyte*)page);
if ((*env)->ExceptionCheck(env)) goto invalid;
r->message.cursor = mf_get64(page); memcpy(r->message.version, page + 8, 16);
}
memcpy(r->message.path, path, strlen(path) + 1); callback = member_files_gui_request;
} else if (op == 5) {
if (server && (!path || !mf_avatar_manifest_valid(path))) goto invalid;
struct chat_avatar_req* r = u_calloc(1, sizeof(*r)); req = r;
if (!r) goto invalid;
r->inst = inst; r->id = id; r->group = group; r->server = server;
if (server) memcpy(r->file, path, strlen(path) + 1);
callback = chat_core_set_my_avatar_trampoline;
} else {
struct mf_cancel_request* r = u_calloc(1, sizeof(*r)); req = r;
if (!r) goto invalid;
r->inst = inst; r->id = id; callback = member_files_gui_cancel;
}
if (path) (*env)->ReleaseStringUTFChars(env, remote, path);
if (file) (*env)->ReleaseStringUTFChars(env, local, file);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "Android avatar: queued operation=%d id=%lld group=%016llx server=%016llx owner=%016llx",
op, (long long)id, (unsigned long long)group, (unsigned long long)server, (unsigned long long)owner);
uasync_post(ua, callback, req); return JNI_TRUE;
invalid:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "Android avatar: invalid arguments/allocation operation=%d id=%lld group=%016llx",
op, (long long)id, (unsigned long long)group);
if (path) (*env)->ReleaseStringUTFChars(env, remote, path);
if (file) (*env)->ReleaseStringUTFChars(env, local, file);
u_free(req); return JNI_FALSE;
}
/* Общие файловые команды. Wire уже UTF-8, поэтому JNI modified UTF-8 не портит имена с emoji. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFileRequest(
JNIEnv* env, jobject thiz, jlong group, jlong server, jbyteArray wire) {
(void)thiz;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
jsize size = wire ? (*env)->GetArrayLength(env, wire) : 0;
struct mf_gui_request* q = NULL;
if (!inst || !ua || !uasync_is_running(ua) || !group || !server || size < MF_WIRE_HEADER || size > MF_WIRE_MAX) goto invalid;
uint8_t bytes[MF_WIRE_MAX];
(*env)->GetByteArrayRegion(env, wire, 0, size, (jbyte*)bytes);
if ((*env)->ExceptionCheck(env)) goto invalid;
q = u_calloc(1, sizeof(*q));
if (!q || mf_decode(bytes, size, &q->message)) goto invalid;
struct mf_message* m = &q->message;
int read = m->command == MF_LIST || m->command == MF_STAT || m->command == MF_QUOTA;
int write = m->command == MF_MKDIR || m->command == MF_RENAME || m->command == MF_REMOVE_PATH;
if (m->kind != MF_REQUEST || (!read && !write) || m->data_len || m->offset || m->size || m->more ||
!mf_path_valid(m->path, read) || (!m->owner && (m->command != MF_LIST || m->path[0])) ||
(m->command == MF_RENAME ? !mf_path_valid(m->target, 0) : m->target[0] != 0)) goto invalid;
if (write && (m->owner != inst->node_id ||
(!strncasecmp(m->path, "avatars", 7) && (!m->path[7] || m->path[7] == '/')))) goto invalid;
if (m->command == MF_RENAME && !strncasecmp(m->target, "avatars", 7) && (!m->target[7] || m->target[7] == '/')) goto invalid;
q->inst = inst; q->group = (uint64_t)group; q->server = (uint64_t)server;
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "Android files: request id=%llu cmd=%s group=%llu server=%llu owner=%llu path=%s",
(unsigned long long)m->request_id, mf_command_text(m->command), (unsigned long long)group,
(unsigned long long)server, (unsigned long long)m->owner, m->path);
uasync_post(ua, member_files_gui_request, q); return JNI_TRUE;
invalid:
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Android files: request rejected group=%llu server=%llu bytes=%d ready=%d",
(unsigned long long)group, (unsigned long long)server, (int)size, inst && ua && uasync_is_running(ua));
u_free(q); return JNI_FALSE;
}
/* Копирование настоящего UTF-8 пути с проверкой длины и отсутствия NUL. */
static int android_file_path(JNIEnv* env, jbyteArray bytes, char* out) {
jsize size = bytes ? (*env)->GetArrayLength(env, bytes) : 0;
if (size < 1 || size > MF_PATH_MAX) return -1;
(*env)->GetByteArrayRegion(env, bytes, 0, size, (jbyte*)out);
if ((*env)->ExceptionCheck(env) || memchr(out, 0, size)) return -1;
out[size] = 0; return 0;
}
/* Handle/локальный файл принадлежат общему transfer, Kotlin ждёт его терминальное событие. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFileTransfer(
JNIEnv* env, jobject thiz, jlong id, jlong group, jlong server, jlong owner,
jbyteArray remote, jbyteArray local, jboolean upload, jbyteArray version) {
(void)thiz;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
struct mf_transfer_request* q = NULL;
if (!inst || !ua || !uasync_is_running(ua) || !id || !group || !server || !owner ||
!version || (*env)->GetArrayLength(env, version) != 16) goto invalid;
q = u_calloc(1, sizeof(*q));
if (!q || android_file_path(env, remote, q->remote) || android_file_path(env, local, q->local) ||
!mf_path_valid(q->remote, 0) || q->local[0] != '/') goto invalid;
(*env)->GetByteArrayRegion(env, version, 0, 16, (jbyte*)q->version);
if ((*env)->ExceptionCheck(env) || (!upload && mf_zero(q->version, 16))) goto invalid;
if (upload && (owner != (jlong)inst->node_id ||
(!strncasecmp(q->remote, "avatars", 7) && (!q->remote[7] || q->remote[7] == '/')))) goto invalid;
q->inst = inst; q->id = id; q->group = group; q->server = server; q->owner = owner; q->upload = upload != JNI_FALSE;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "Android files: transfer queued id=%llu group=%llu server=%llu owner=%llu upload=%d path=%s",
(unsigned long long)id, (unsigned long long)group, (unsigned long long)server, (unsigned long long)owner, q->upload, q->remote);
uasync_post(ua, member_files_gui_transfer, q); return JNI_TRUE;
invalid:
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Android files: transfer rejected id=%llu group=%llu server=%llu owner=%llu upload=%d",
(unsigned long long)id, (unsigned long long)group, (unsigned long long)server, (unsigned long long)owner, upload);
u_free(q); return JNI_FALSE;
}
/* Отмена по уникальному ID не затрагивает передачи других владельцев. */
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeFileCancel(JNIEnv* env, jobject thiz, jlong id) {
(void)env; (void)thiz;
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
struct UASYNC* ua = instance_lite_get_uasync();
if (!id || !inst || !ua || !uasync_is_running(ua)) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "Android files: cancel unavailable id=%llu", (unsigned long long)id); return JNI_FALSE;
}
struct mf_cancel_request* q = u_calloc(1, sizeof(*q));
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Android files: cancel allocation failed"); return JNI_FALSE; }
q->inst = inst; q->id = id;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "Android files: cancel queued id=%llu", (unsigned long long)id);
uasync_post(ua, member_files_gui_cancel, q); return JNI_TRUE;
}
#endif /* __ANDROID__ */