diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index 6a7a2624..419566e9 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -445,6 +445,19 @@ void topo_group_new_conn(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { if (!conn->instance) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance is NULL"); return; } if (!conn->instance->rt) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "conn->instance->rt is NULL"); return; } + /* дедуп: тот же conn стреляет ETCP_CONN_STATUS_UP дважды (UDP-линк, затем TCP-линк). + * Если conn уже в senders_list — повторно не обрабатываем, иначе active_conn_count + * задваивается и переподключение никогда не стартует. */ + { struct ll_entry* se = group->senders_list ? group->senders_list->head : NULL; + while (se) { + if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) { + DEBUG_INFO(DEBUG_CATEGORY_BGP, "topo_group_new_conn: conn already in senders_list, skip double UP (%s)", conn->log_name); + return; + } + se = se->next; + } + } + topo_recovery_cancel_for_node(group, conn->peer_node_id); struct TOPO_GROUP_NODE* peer_nq = topo_node_find_by_id(group, conn->peer_node_id); diff --git a/src/routing_layer/topo_group_connect.c b/src/routing_layer/topo_group_connect.c index 9ed3446b..34e4789e 100644 --- a/src/routing_layer/topo_group_connect.c +++ b/src/routing_layer/topo_group_connect.c @@ -59,6 +59,7 @@ struct TOPO_GROUP_CONNECT { int candidate_count; struct CONN_MGR_HANDLE* handles[TGC_MAX_HANDLES]; int handle_count; + struct CONN_MGR_HANDLE* cur_handle; /* handle текущей попытки Phase 2/3 (для закрытия на TIMEOUT) */ }; static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc); @@ -72,6 +73,12 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g /* ─── нотификация GUI о списке узлов в процессе подключения ─── */ +/** + * Отправляет в GUI событие CHAT_EVT_CONNECTING_NODES — список узлов, к которым + * сейчас идёт попытка подключения. GUI по нему рисует жёлтый кружок у этих узлов. + * Пустой список (count=0) означает «сейчас ни к кому не подключаемся» — + * все жёлтые кружки снимаются. + */ static void tgc_notify_connecting(struct TOPO_GROUP_CONNECT* gc, const uint64_t* ids, int count) { size_t cl = strlen(gc->group->channel_id); if (cl > 255) cl = 255; size_t sz = 1 + cl + 2 + (size_t)count * 8; @@ -82,7 +89,7 @@ static void tgc_notify_connecting(struct TOPO_GROUP_CONNECT* gc, const uint64_t* for (int i = 0; i < count; i++) { memcpy(p, &ids[i], 8); p += 8; } chat_event_post(CHAT_EVT_CONNECTING_NODES, buf, (int)sz); u_free(buf); - DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: notify_connecting ch=%s count=%d", + DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: notify_connecting ch=%s count=%d", TGC_ID, gc->group->channel_id, count); } @@ -90,6 +97,12 @@ static void tgc_notify_connecting(struct TOPO_GROUP_CONNECT* gc, const uint64_t* * Жизненный цикл * ══════════════════════════════════════════════════════════════════════ */ +/** + * Запуск авто-подключения к узлам CHAT-группы. + * Если модуль уже запущен — останавливает его и стартует заново с нуля. + * Начинает с Phase 1: параллельные попытки ко всем пирам, что были connected + * в прошлой сессии (из БД). Если таких нет — сразу переходит к Phase 2. + */ int topo_group_connect_init(struct TOPO_GROUP* group) { if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT || !group->channel_id[0]) return -1; @@ -125,6 +138,11 @@ int topo_group_connect_init(struct TOPO_GROUP* group) { return 0; } +/** + * Полная остановка авто-подключения: отменяет все таймеры, закрывает все + * открытые соединения и освобождает память. Вызывается при удалении группы + * и при перезапуске (init/restart). + */ void topo_group_connect_destroy(struct TOPO_GROUP* group) { struct TOPO_GROUP_CONNECT* gc = group->connect; if (!gc) return; @@ -132,10 +150,16 @@ void topo_group_connect_destroy(struct TOPO_GROUP* group) { if (gc->phase_timer) { uasync_cancel_timeout(group->instance->ua, gc->phase_timer); gc->phase_timer = NULL; } if (gc->pause_timer) { uasync_cancel_timeout(group->instance->ua, gc->pause_timer); gc->pause_timer = NULL; } for (int i = 0; i < gc->handle_count; i++) conn_mgr_close(gc->handles[i]); + if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } u_free(gc->candidate_ids); u_free(gc); } +/** + * Внешний перезапуск авто-подключения (destroy + init). Используется, например, + * при смене сети, когда надо начать поиск заново. Не трогает модуль, если уже + * есть живые соединения. + */ void topo_group_connect_restart(struct TOPO_GROUP* group) { struct TOPO_GROUP_CONNECT* gc = group->connect; if (!gc) { DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "%s: restart ch=%s no gc → init", TGC_ID, group->channel_id); topo_group_connect_init(group); return; } @@ -145,6 +169,10 @@ void topo_group_connect_restart(struct TOPO_GROUP* group) { topo_group_connect_init(group); } +/** + * Сколько сейчас живых соединений с пирами группы. Нужно внешней логике + * (например, chat_sync) для решения, запускать ли переподключение. + */ int topo_group_connect_active_count(struct TOPO_GROUP* group) { struct TOPO_GROUP_CONNECT* gc = group->connect; return gc ? gc->active_conn_count : 0; @@ -154,6 +182,11 @@ int topo_group_connect_active_count(struct TOPO_GROUP* group) { * ON UP / DOWN * ══════════════════════════════════════════════════════════════════════ */ +/** + * Обработка появления соединения с пиром. Увеличивает счётчик активных и + * помечает connected=1 в БД (чтобы при следующем старте узел попал в Phase 1). + * Пропускает, если пир уже подключён через другое соединение. + */ void topo_group_connect_on_up(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT || !group->channel_id[0] || !conn) return; uint64_t peer = conn->peer_node_id; @@ -178,6 +211,11 @@ void topo_group_connect_on_up(struct TOPO_GROUP* group, struct ETCP_CONN* conn) TGC_ID, (unsigned long long)peer, group->channel_id, (unsigned long long)group->group_id, gc->active_conn_count); } +/** + * Обработка обрыва соединения. Уменьшает счётчик активных и помечает + * connected=0 в БД. Если живых соединений не осталось — немедленно + * перезапускает цикл поиска с Phase 1. + */ void topo_group_connect_on_down(struct TOPO_GROUP* group, struct ETCP_CONN* conn) { if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT || !group->channel_id[0] || !conn) return; uint64_t peer = conn->peer_node_id; @@ -224,10 +262,16 @@ void topo_group_connect_on_down(struct TOPO_GROUP* group, struct ETCP_CONN* conn * Единый коллбэк для всех фаз * ══════════════════════════════════════════════════════════════════════ */ +/** + * Единый коллбэк на результат каждой попытки подключения (успех/таймаут/обрыв). + * В Phase 1 — просто считает результаты (итог подводит tgc_phase1_timeout). + * В Phase 2/3 — успех завершает цикл; провал закрывает handle (чтобы не оставить + * залипшее состояние) и ставит паузу перед следующей попыткой. + */ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_id, enum conn_mgr_event event, void* arg) { struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg; - (void)h; (void)group_id; + (void)group_id; int ok = (event == CONN_EVENT_UP); @@ -253,6 +297,7 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g tgc_notify_connecting(gc, NULL, 0); } else { DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase2 0x%016llx FAIL — pause then try next", TGC_ID, (unsigned long long)node_id); + if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } tgc_start_pause(gc, (void(*)(void*))tgc_phase2_try_next, "tgc_p2_try"); } break; @@ -263,6 +308,7 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g tgc_notify_connecting(gc, NULL, 0); } else { DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase3 0x%016llx FAIL — pause then try next", TGC_ID, (unsigned long long)node_id); + if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } tgc_start_pause(gc, (void(*)(void*))tgc_phase3_try_next, "tgc_p3_try"); } break; @@ -273,6 +319,12 @@ static void tgc_callback(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t g * Пауза: 1s ACTIVE / 30s STANDBY * ══════════════════════════════════════════════════════════════════════ */ +/** + * Пауза между попытками подключения. Длительность зависит от активности + * приложения: 1 секунда когда приложение активно, 30 секунд когда в фоне + * (экран выключен). После паузы вызывает cb (следующая попытка или перезапуск + * цикла). На время паузы уведомляет GUI пустым списком (жёлтые кружки гаснут). + */ static void tgc_start_pause(struct TOPO_GROUP_CONNECT* gc, void (*cb)(void*), const char* label) { gc->phase = TGC_PHASE_PAUSE; int active = (gc->group->instance->client_activity == CLIENT_ACTIVITY_ACTIVE); @@ -287,6 +339,11 @@ static void tgc_start_pause(struct TOPO_GROUP_CONNECT* gc, void (*cb)(void*), co * Phase 1 timeout * ══════════════════════════════════════════════════════════════════════ */ +/** + * Таймаут параллельных попыток Phase 1 (2 секунды). Закрывает неудачные + * handles и сбрасывает connected в БД. Если хоть один узел подключился — + * цикл завершён; иначе переходим к Phase 2. + */ static void tgc_phase1_timeout(void* arg) { struct TOPO_GROUP_CONNECT* gc = (struct TOPO_GROUP_CONNECT*)arg; gc->phase_timer = NULL; @@ -330,6 +387,11 @@ static void tgc_phase1_timeout(void* arg) { * Phase 2 * ══════════════════════════════════════════════════════════════════════ */ +/** + * Последовательный перебор узлов с публичными/EIM адресами (сначала supernode, + * затем обычные публичные). Пробует следующий узел и ставит ему жёлтый кружок. + * Когда все перебраны — переходит к Phase 3. + */ static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc) { if (gc->phase == TGC_PHASE_PAUSE) gc->phase = TGC_PHASE_TWO; if (gc->phase != TGC_PHASE_TWO) return; @@ -358,7 +420,7 @@ static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc) { uint64_t nid = gc->candidate_ids[gc->cursor++]; DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase2 trying 0x%016llx (%d/%d) super_round=%d", TGC_ID, (unsigned long long)nid, gc->cursor, gc->candidate_count, gc->tried_super); - if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, NULL) < 0) continue; + if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, &gc->cur_handle) < 0) continue; tgc_notify_connecting(gc, &nid, 1); return; } @@ -371,6 +433,11 @@ static void tgc_phase2_try_next(struct TOPO_GROUP_CONNECT* gc) { * Phase 3 — локальные/strict NAT адреса * ══════════════════════════════════════════════════════════════════════ */ +/** + * Последовательный перебор узлов с локальными/strict NAT адресами (как Phase 2, + * но для узлов, доступных только в локальной сети). Когда все перебраны — + * пауза и полный перезапуск цикла с Phase 1 (бесконечно, пока не подключимся). + */ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) { if (gc->phase == TGC_PHASE_PAUSE) gc->phase = TGC_PHASE_THREE; if (gc->phase != TGC_PHASE_THREE) return; @@ -386,7 +453,7 @@ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) { uint64_t nid = gc->candidate_ids[gc->cursor++]; DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: Phase3 trying 0x%016llx (%d/%d)", TGC_ID, (unsigned long long)nid, gc->cursor, gc->candidate_count); - if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, NULL) < 0) continue; + if (conn_mgr_open_invite(gc->group->instance, gc->group->group_id, NULL, nid, tgc_callback, gc, &gc->cur_handle) < 0) continue; tgc_notify_connecting(gc, &nid, 1); return; } @@ -398,10 +465,17 @@ static void tgc_phase3_try_next(struct TOPO_GROUP_CONNECT* gc) { * Полный перезапуск цикла с Phase 1 (без destroy/init) * ══════════════════════════════════════════════════════════════════════ */ +/** + * Полный перезапуск цикла с Phase 1 без уничтожения модуля. Закрывает все + * открытые соединения, сбрасывает состояние и заново запускает параллельные + * попытки ко всем connected-пирам из БД. Вызывается при обрыве всех соединений + * и после исчерпания Phase 3. + */ static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc) { gc->pause_timer = NULL; if (gc->phase_timer) { uasync_cancel_timeout(gc->group->instance->ua, gc->phase_timer); gc->phase_timer = NULL; } for (int i = 0; i < gc->handle_count; i++) conn_mgr_close(gc->handles[i]); + if (gc->cur_handle) { conn_mgr_close(gc->cur_handle); gc->cur_handle = NULL; } gc->handle_count = 0; gc->pending = 0; gc->connected_count = 0; gc->cursor = 0; gc->tried_super = 0; u_free(gc->candidate_ids); gc->candidate_ids = NULL; gc->candidate_count = 0; diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt index dbbb8892..4bfbc72a 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt @@ -146,4 +146,14 @@ class MainActivity : ComponentActivity() { } } } + + override fun onStart() { + super.onStart() + if (NativeLib.isReady()) NativeLib.setActive(true) + } + + override fun onStop() { + super.onStop() + if (NativeLib.isReady()) NativeLib.setActive(false) + } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt index 00ac653b..efd1edc2 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt @@ -68,7 +68,7 @@ data class ChatMember(val nodeId: Long, val name: String, val online: Boolean = val directUp: Boolean = false, val indirectUp: Boolean = false, val directPresence: Boolean = false, val indirectPresence: Boolean = false, val bgpPresence: Boolean = false, val bgpRtt: Int = 0, - val bgpNodes: Int = 0) { + val bgpNodes: Int = 0, val connecting: Boolean = false) { val flags: Int get() = (if (isSuper) 1 else 0) or (if (isAdmin) 2 else 0) or (if (isModer) 4 else 0) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index e82449c3..298700ce 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -53,6 +53,9 @@ object NativeLib { fun ping() { nativePing() } fun isResponsive(): Boolean = nativeIsResponsive() + /* ── App foreground/background (Android lifecycle) ── */ + fun setActive(active: Boolean) { nativeSetActive(active) } + fun sendMessage(channelId: String, text: String) { nativeSendMessage(channelId, text) } fun createChannel(name: String, channelId: String) { nativeCreateChannel(name, channelId) } fun connectNode(address: String, port: Int, pubkeyHex: String) { nativeConnectNode(address, port, pubkeyHex) } @@ -140,6 +143,7 @@ object NativeLib { private external fun nativePing() private external fun nativeIsResponsive(): Boolean private external fun nativeIsRunning(): Boolean + private external fun nativeSetActive(active: Boolean) /* ── Channel members JNI ── */ private external fun nativeGetChannelMembers(channelId: String): String? diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt index 139ba0db..f5027297 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt @@ -122,11 +122,13 @@ private fun MemberListItem( onEdit: () -> Unit ) { val onlineColor = when { + member.connecting -> Color(0xFFFFEB3B) // yellow: connecting member.directUp || member.indirectUp -> Color(0xFF4CAF50) // green: up member.directPresence || member.indirectPresence || member.bgpPresence -> Color(0xFFFFA726) // orange: presence but down else -> Color(0xFF9E9E9E) // gray: offline } val connLabel = when { + member.connecting -> "connecting" member.directUp -> "direct" member.indirectUp -> "indirect" member.directPresence || member.indirectPresence || member.bgpPresence -> "offline" @@ -171,6 +173,7 @@ private fun MemberListItem( if (member.isSuper) parts.add("Super") if (member.isStorage) parts.add("Storage") if (member.isSelf) parts.add("self") + else if (member.connecting) parts.add("connecting…") else if (member.directUp) parts.add("direct↑") else if (member.indirectUp) parts.add("indirect↑") else if (member.directPresence || member.indirectPresence || member.bgpPresence) parts.add("down") diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index 7b679f7b..14c476a0 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -65,6 +65,9 @@ class ChatViewModel : ViewModel() { private val _nodeStatuses = MutableStateFlow>(emptyMap()) val nodeStatuses: StateFlow> = _nodeStatuses + private val _connecting = MutableStateFlow>>(emptyMap()) + val connecting: StateFlow>> = _connecting + private val _localSockets = MutableStateFlow>(emptyList()) val localSockets: StateFlow> = _localSockets @@ -281,6 +284,24 @@ class ChatViewModel : ViewModel() { _selectedMember.value = _selectedMember.value?.copy(online = ml.connected && ml.linksUp > 0) } } + 27 -> { /* CONNECTING_NODES: [ch_id_len:1][ch_id:var][count:2][node_id:8]* */ + if (data == null || data.size < 3) return + val cl = data[0].toInt() and 0xFF + if (data.size < 1 + cl + 2) return + val chId = String(data, 1, cl) + val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, data.size - 1 - cl).order(java.nio.ByteOrder.LITTLE_ENDIAN) + val count = buf.getShort().toInt() and 0xFFFF + val set = HashSet() + for (i in 0 until count) { + if (buf.position() + 8 > data.size) break + set.add(buf.getLong()) + } + _connecting.value = _connecting.value + (chId to set) + val cur = _currentChannel.value + if (cur != null && cur.id == chId) { + _members.value = _members.value.map { it.copy(connecting = it.nodeId in set) } + } + } } } @@ -324,17 +345,19 @@ class ChatViewModel : ViewModel() { fun refreshMembers(chId: String) { val r = repo ?: return val statuses = _nodeStatuses.value + val connecting = _connecting.value[chId] ?: emptySet() viewModelScope.launch { val raw = withContext(Dispatchers.IO) { r.getMembers(chId) } _members.value = raw.map { m -> val ns = statuses[m.nodeId] - if (ns != null) m.copy( + val base = if (ns != null) m.copy( online = ns.online, rtt = ns.bestRtt, directUp = ns.directUp, indirectUp = ns.indirectUp, directPresence = ns.directPresence, indirectPresence = ns.indirectPresence, bgpPresence = ns.bgpPresence, bgpRtt = ns.bgpRtt, bgpNodes = ns.bgpNodes ) else m + base.copy(connecting = base.nodeId in connecting) } } } diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index e7e4b5bd..d55e85c6 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -1177,6 +1177,20 @@ void utun_bridge_on_network_change(void) { 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 = chat_core_get_inst(); + if (!inst) return; + utun_set_client_activity(inst, active); + 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); +} + void utun_bridge_collect_conn_list(void) { instance_lite_collect_conn_list(); } @@ -1990,6 +2004,14 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeOnNetworkChanged( 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); +} + /* ── Connection monitor ── */ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCollectConnList( diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.h b/tools/chatgui-android/jni_bridge/android_jni_bridge.h index 64bc9b48..cbde4f87 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.h +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.h @@ -133,6 +133,10 @@ char* utun_bridge_get_local_sockets_json(void); void utun_bridge_on_network_change(void); +/* ── App foreground/background (Android lifecycle) → client_activity ── */ + +void utun_bridge_set_active(int active); + /* ── Connection monitor (post collect functions to uasync thread) ── */ void utun_bridge_collect_conn_list(void);