Files
utun2/tools/chatgui-android/doc/ARCHITECTURE.md
T

22 KiB
Raw Blame History

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 — за аудио. Сборка использует Gradle 8.13, AGP 8.13.2, Kotlin/Compose compiler 2.2.20 и CameraX 1.7.0-rc01; минимальный Android приложения остаётся API 26.

CameraRecordScreen объединяет режимы «Фото / Видео» для канала и PM. Адресат, поколение PM и канальный ответ фиксируются до открытия камеры. Фото запрашивает только CAMERA; RECORD_AUDIO требуется в режиме видео. Привязывается Preview и один выбранный use case (ImageCapture либо VideoCapture). После снимка доступны просмотр и пересъёмка; переключение режима разрешено в состоянии готовности. Экран удаляет отменённые cache-файлы и игнорирует запоздавшие callbacks. При отправке владение файлом переходит ViewModel, а после принятия JPEG/PNG — attachment_send.

Общие с трансляцией иконки открывают меню камеры, разрешения, кодека и битрейта. Видеозапись использует H.264/H.265 и AAC в MP4; доступные кодеки и разрешения проверяются через Recorder.getVideoCapabilities(cameraInfo, mime) для выбранной камеры. Разрешение и битрейт независимы. Перед готовностью/отправкой MediaExtractor проверяет, что фактический MIME видеодорожки совпадает с выбранным кодеком. В фото отдельное меню JPEG quality (10..100); ImageCapture.setJpegQuality() задаёт сжатие при съёмке. Предпросмотр читает полученный JPEG с EXIF и сохраняет артефакты сжатия; масштабирование позволяет рассмотреть их. Декодирование ограничено 4096 по большей стороне, на SM-A525F штатные снимки читаются без уменьшения.

JPG/JPEG/PNG из выбора файла и камеры проходят общий sendPhotoMessage: фактический MIME и размеры с учётом EXIF читаются в IO, затем передаются через JNI. В PM метаданные JPEG/PNG шифруются вместе с остальными метаданными вложений. Превью декодируется с ограничением размера; подпись содержит разрешение и размер файла. Каналы и PM используют PhotoViewerScreen с масштабированием и перемещением.

Владение ядром и потоки

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:

[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.

LogManager хранит последние 100000 строк в памяти процесса, в порядке приёма. Прямой Kotlin addLog сохраняет важность logcat, но не проходит native-фильтр и не доставляет строку в native-файл/UDP. videoDebug/videoTrace проходят общий video-фильтр через JNI. JNI callback получает уровни bridge 0=TRACE..4=ERROR, а не числовые DEBUG_LEVEL_*; неизвестный уровень фиксируется предупреждением.

Строки логов передаются через onNativeLog(int, byte[], byte[]) как обычный UTF-8: это сохраняет кириллицу/emoji без ограничений Modified UTF-8 NewStringUTF. Повреждённые UTF-8-байты Kotlin отображает как U+FFFD. На каждый вызов JNI берёт свой local ref callback под mutex; замена/очистка global ref не инвалидирует уже начатую доставку. Java-код вызывается после освобождения mutex. Указатель C log callback читается/меняется атомарно; отключение не ждёт начатых вызовов. init включает пересылку, Activity подписывается на экране логов и отключает её при выходе; stop снимает C callback, destroy дополнительно освобождает global ref.

Ошибки JNI-доставки и повторный вход в callback того же потока фиксируются напрямую в logcat, минуя debug hook, чтобы исключить рекурсию. Проверяются attach, ссылки, method lookup, выделение/копирование массивов и Java callback. Все свои local refs освобождаются; detach выполняется только после своего attach. Исключение от собственной операции/callback описывается и снимается; исключение, уже существовавшее при входе в logger, сохраняется, доставка этой строки пропускается. Неудачная доставка не повторяется. Native logcat/файл/UDP работают независимо.

Экран логов показывает последние 10000 строк истории, обновляя снимок не чаще кадра. StateFlow передаёт объединяемую ревизию, строки читаются из общей истории: задержка UI и открытие экрана после последнего сообщения не теряют строки в отдельной очереди событий. Лимиты истории/экрана всё равно вытесняют старые строки. Фильтр выбирает точный уровень; пауза отключает только прокрутку, сбор продолжается. Автопрокрутка следует последней видимой строке после фильтрации и возобновления.

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. На Linux :app:testDebugUnitTest также запускает 25 fault-сценариев настоящего JNI logger через :app:testJniLog; окружение и границы модели: tests/jni_log/README.md. Проверки native bridge/standby доступны в tests/Makefile.am; проверка независимости media workers на устройстве описана в ../tests/stream_worker_smoke/README.md и не проверяет сетевую доставку/UI.