# chatgui-android — архитектура ## Состав и сборка Android-клиент использует общее ядро из `lib/` и `src/`: ETCP с UDP-линками и STCP-линками поверх TCP, NCD, группы/BGP, ETCP-router, чат, DM и media-сервисы. Отдельной Android-реализации chat_sync/merkle_sync и отдельного lite-instance нет. STCP передаёт ETCP-пакеты; его потоковое шифрование — AES-CTR, пакетное ETCP — AES-CCM. Контракты транспорта описаны в `../../../src/transport_layer/stcp.h` и `etcp.h`. `libutun_lite/utun_sources.cmake` собирает `lib/*.c`, LMDB и рекурсивно `src/*.c`, исключая `utun.c`, каталоги lwip_orig/uip/transport_layer/BBR и файлы отдельного голосового стека. Это источник актуального состава, а не список «только TCP». Файлы Android-обёртки перечислены в `_cfg_src`; старая копия `invite_link_c.c` туда не входит. NDK-сборка в `app/src/main/cpp/CMakeLists.txt` создаёт OBJECT-библиотеку `utun_core` и итоговую `libutun.so`: ядро, JNI, UDP-лог, отдельная статическая `utun_voice`, OpenSSL Crypto, Opus, SpeexDSP, ONNX Runtime. JNI распределён между `android_jni_bridge.c`, `android_stream_jni.c`, `android_call_video_jni.c` и `android_call_output_jni.c`. Голосовой стек собирается по `src/call/voice_sources.cmake`. Параметры приложения определяет `app/build.gradle.kts`: minSdk 26, compile/targetSdk 36, NDK 29.0.14206865, JVM 17, ABI arm64-v8a/x86_64. UI — Kotlin, Jetpack Compose и Material3; CameraX отвечает за захват фото/видео, MediaCodec — за live video, AudioRecord/AudioTrack — за аудио. ## Владение ядром и потоки `NativeLib.init(dbPath, controlPort)` сохраняет config/log callbacks и настраивает voice_recorder. Он не запускает ядро и не открывает control-listener; controlPort в этой ветке только логируется. Затем `NativeLib.start(configText)` передаёт INI в `instance_lite_start()`. Один worker pthread создаёт uasync и `UTUN_INSTANCE`, принудительно задаёт CLIENT_TYPE_MOBILE и запускает `utun_core_start()` + `chat_service_start()`. UTUN-сервис включается отдельно через `[gui] utun_enabled=1`, по умолчанию выключен. Наличие TUN/routing/NAT-кода в сборке само по себе сервис не включает. До `CHAT_EVT_SERVICE_STARTED` worker привязывает stream bridge и call_video_port, а при UTUN_WITH_VOICE — call_audio. Успех start означает создание потока либо уже существующий запуск; он не подтверждает готовность сервисов. Готовность проверяется по событию и `instance_lite_is_running()`. Команды UI обычно копируют аргументы и ставят работу в uasync. При этом C JSON-getters и некоторые настройки читают SQLite/живые структуры синхронно в вызывающем потоке; C `utun_bridge_set_member_nick()` пишет локальный nickname напрямую. C setter chat_setting также синхронный, тогда как JNI nativeSetChatSetting использует trampoline. Нельзя считать весь bridge автоматически выполняющимся в потоке ядра. Заимствованные inst/ua не удерживают ресурс: frontend должен сериализовать вызовы по ним с stop/restart; обход живых списков требует исключить их параллельное изменение. JNI получает JNIEnv для native-потока и вызывает Kotlin `onEvent(type, ByteArray?)`. Payload копируется в byte array; его формат зависит от CHAT_EVT_* из `src/chat/chat_event.h`. Это не универсальный JSON. Отдельный C `utun_event_fn` не является обработчиком бинарных событий instance_lite. ViewModel переносит обновление UI в свой coroutine context. Перед stop/restart Kotlin сбрасывает controllers и останавливает внешние media workers. Worker снимает media-привязки, уничтожает instance, снимает standby/UDP-таймеры и уничтожает uasync. `instance_lite_stop()` ждёт до двух секунд: при timeout отсоединяет поток, поэтому возврат не гарантирует завершённый cleanup. Без опубликованного instance он возвращается сразу. Restart пересоздаёт instance и uasync. Дополнительный опрос is_running в bridge до 5с может увидеть ещё старое работающее ядро; подтверждение нового запуска — SERVICE_STARTED. Общие правила освобождения: `../../../doc/service_lifecycle.md`. ## Данные и конфигурация Ядро владеет общей SQLite `db_path/chats.db`; путь без db_path даёт in-memory БД. Chat использует тот же handle, что и топология. Схему создают общие модули `src/routing_layer/topo_node_sqlite.c`, `src/chat/`, `src/dm/` и media-подсистемы. Room/DbRepository и отдельного Kotlin SQL-соединения в приложении нет: ChatRepository получает JSON через JNI и разбирает события, ViewModel хранит StateFlow. Имена/форматы таблиц не следует дублировать в Android-коде. ConfigProvider хранит настройки в DataStore, читает ключи и формирует общий INI через buildConfigText/buildConfigFromServers. Callback-ключи вроде `node.name` и `db.path` не являются именами INI-секций. Native config API копирует таблицу callbacks по значению; функции и их окружение остаются у провайдера. Init без config и destroy снимают прежнюю регистрацию; смену провайдера нужно сериализовать с cfg_get_*. Минимальный пример INI: ```ini [global] my_node_name=Android # При отсутствии ключей начальный запуск сгенерирует X25519-пару. db_path=/data/user/0/com.utun.chat/files db_sync_enabled=1 chatserver_enabled=1 auto_sockets=android [gui] utun_enabled=0 [allowed_keys] allow_all=yes [ntp] enabled=no [debug] chat=info socket=info ``` Секции `[server:<имя>]` с addr/type/transport/netif строятся из интерфейсов устройства. NetworkChangeMonitor передаёт новый INI в updateSockets: uasync согласует сокеты через `auto_socket_reload_config`, сохраняя ядро/чат. В auto_sockets=android общий `auto_socket_on_network_change()` не запускает scan: нужен именно reload из Kotlin. Настройки голоса, рации, stream, standby и хранения передаются в `[chat]`, UDP-лог — в `[log_udp] ip/port`; полный набор задаёт ConfigProvider. Начальная генерация ключей пытается сохранить их в `db_path/keys.conf`. `instance_lite_regenerate_keys()` меняет ключи/node_id на месте, без пересоздания соединений. ## Каналы, приглашения и DM Создание канала ставится в uasync; channel_id, переданный старому bridge API создания, игнорируется: ядро генерирует ID. `connect_node(address,port,pubkey)` остался заглушкой. Для подключения канала/участника используются общие chat_core/topo_group API. `InviteLink.kt` получает предпросмотр через `NativeLib.decodeInvite()` — JNI вызывает общий `src/chat/invite_link.c`. Ссылка `utun://...` содержит версию 0x03, пароль, channel_id, join_key, REALITY-параметры и блоки публичного ключа/адресов. JOIN передаёт исходную ссылку и request ID через потокобезопасный chat_join_port; принятый запрос завершается CONNECT_RESULT с тем же request. Отрицательный возврат submit означает отказ без события. Детали: `src/chat/chat_join.h`, `chat_join_port.h`. Регистрация join_key подтверждается до выдачи ссылки. JOIN добавляет подписанного мембера, Merkle распространяет запись; доступ к группе определяется локальной таблицей участников. JOIN_READY добавления и READY групповой сессии — разные состояния. DM-команды используют conv_id и поколение беседы. История приходит событием CHAT_EVT_DM_MESSAGES, список — CHAT_EVT_DM_CONVERSATIONS. Frontend отбрасывает ответы старого поколения/другой беседы. Voice/video принимают target `dm:`; отдельные dm_* функции принимают conv_id без префикса. Общее устройство DM: `../../../doc/dm_arch.md`. ## Вложения и media Канальные text/file/photo используют chat_core_submit_trampoline. reply_to — ровно 32 байта исходного message_id, нули означают отсутствие ответа. request_id связывает публикацию с CHAT_EVT_MSG_SUBMITTED; успешная постановка не означает доставку пиру. Обычный файл и готовое фото остаются у caller и должны жить неизменными до завершения асинхронного копирования/индексации. Фото здесь не декодируется и не масштабируется. voice_recorder принимает int16 mono 48kHz, отсоединяет PCM/compressor при stop и передаёт общий attachment_send. Запись короче 500мс по монотонным часам отбрасывается. Готовый MP4 передаётся без транскода; при принятом video-запросе cache-файл принадлежит задаче и удаляется также при ошибке/отмене. То же правило у отдельной DM-отправки файла. При отказе постановки файл остаётся caller. Generation защищает публикацию в DM; канальный reply_to/request_id фиксируется до начала подготовки. Звонок и рация используют общий C голосовой стек. Kotlin владеет AudioRecord/AudioTrack, feed/pull workers и маршрутом Android Telecom; DSP и сетевой сигналинг разделены. Вызовы lifecycle DSP выполняются после остановки его внешних feed/pull callers. Контракты PCM/PTS: `src/call/call_audio.h`, `src/radio/radio_audio.h`, `jni_bridge/android_jni_bridge.h`. Live stream bridge владеет одним publisher и независимыми viewers поверх общих `stream_publisher`/`stream_viewer`. UI использует session IDs, не inst/ua. Данные идут через pull, а каталог источников — через CHAT_EVT_STREAM_SOURCES. Pull возвращает owned буфер записей с 28-байтовым BE-заголовком; освобождение — u_free. Demand ограничивает батч viewer; GAP/CLOSED требуют сбросить прежний frontend-буфер. Точный формат, лимиты и срок жизни signal: `libutun_lite/stream_bridge.h`. ## Фон и диагностика Android HeadlessService — foreground service с уведомлением и общим native-ядром, а не Linux control-loop. Он запускает NetworkChangeMonitor и NetworkActivityMonitor. Переход UI в фон передаёт client_activity в uasync. Standby чередует ACTIVE/SLEEP; сам event loop продолжает работать. Ожидания SLEEP будятся после перехода в ACTIVE, сетевой activity может разбудить после min_sleep. Контракт отмены: `libutun_lite/standby.h`. DEBUG_* фильтруются общими уровнями/категориями. Bridge переводит их в logcat и, при включённой подписке, Kotlin LogManager. Debug hook также пишет в UDP-буфер: 16KiB threshold либо timer 200мс, неблокирующая отправка без повторов. ERROR вызывает flush. Target — числовой IPv4; attach/detach таймера выполняет uasync worker при каждом запуске/рестарте. Ошибки самого UDP-логгера идут в отдельный utun-udplog, минуя debug hook. ## Linux headless и проверки Каталог `headless/` — отдельный каркас: headless_main запускает select-loop и JSON-line control socket, но не создаёт uasync/instance и не запускает чат. `-p` меняет порт, `-l` только печатает имя файла. ping/status/connections/debug_level возвращают заглушки, subscribe/quit управляют клиентом, ряд команд даёт not implemented. Текстовые команды вида `send ` этот каркас не поддерживает. JOIN/settings требуют уже запущенное ядро и исполнения в его потоке; каркас не заменяет рабочий headless API. Для Linux сетевых сценариев используются основной `src/utun`, `[chatserver] headless_control_bind=127.0.0.1:9999` и `tools/chatcli`. Для APK: `cd tools/chatgui-android && ./gradlew assembleDebug`. Проверки native bridge/standby доступны в `tests/Makefile.am`; проверка независимости media workers на устройстве описана в `../tests/stream_worker_smoke/README.md` и не проверяет сетевую доставку/UI.