190 lines
15 KiB
Markdown
190 lines
15 KiB
Markdown
# 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:<conv_id>`;
|
||
отдельные 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 <channel> <text>` этот каркас не поддерживает. 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.
|