# chatgui-android — План реализации ## Порядок этапов ``` Этап 1 ── Структура проекта + CMake │ ▼ Этап 2 ── libutun_lite (C-ядро) │ ▼ Этап 3 ── Headless CLI + control socket ◄── первый работающий артефакт │ тестируется на Linux ▼ Этап 4 ── JNI bridge (C ↔ Kotlin) │ ▼ Этап 5 ── Android UI (Jetpack Compose) ◄── второй артефакт │ ▼ Этап 6 ── Android Headless Service ◄── третий артефакт │ ▼ Этап 7 ── End-to-end тесты ``` Ключевой принцип: **сначала работает headless → потом UI**. Сетевой стек отлаживается на Linux без Android. Только когда P2P чат работает между двумя headless-экземплярами — переходим к Android. --- ## Этап 1. Структура проекта и сборочная система **Цель:** Собрать пустой проект, который компилируется (пока без логики). ### 1.1. Создать файлы ``` tools/chatgui-android/ ├── CMakeLists.txt # корневой: add_subdirectory(libutun_lite; headless) ├── libutun_lite/ │ ├── CMakeLists.txt # libutun_lite.a из выбранных .c │ └── utun_lite.h # заглушка API └── headless/ ├── CMakeLists.txt # headless binary └── headless_main.c # int main() { return 0; } ``` ### 1.2. libutun_lite/CMakeLists.txt Список всех нужных .c файлов с путями к `../../src/` и `../../lib/`. Использовать `file(GLOB ...)` для категорий (lib/*.c, src/transport_layer/*.c, src/chat/*.c), затем `list(REMOVE_ITEM ...)` исключить ненужные. Результат — статическая библиотека `libutun_lite.a`. ### 1.3. headless/CMakeLists.txt Линкует `headless_main.c` + `libutun_lite.a` + `OpenSSL::Crypto` + `pthread` + `dl`. ### 1.4. Проверка ```bash cd tools/chatgui-android && mkdir -p build && cd build cmake .. && make -j4 ``` Должно: собраться без ошибок, `./headless/headless` выходит с 0. --- ## Этап 2. Минимальное C-ядро (libutun_lite) **Цель:** `libutun_lite.a` собирается из отобранных .c файлов без ошибок компиляции. ### 2.1. Выверка списка файлов Пройти по каждому .c из списка в `ARCHITECTURE.md §9`, проверить что: - Все `#include` разрешаются (пути к lib/ и src/ корректны) - Нет зависимостей от исключённых модулей (etcp, tun, bgp, ...) - Если есть зависимости — либо исключить файл, либо застабить ### 2.2. Застабить зависимости Некоторые файлы могут ссылаться на исключённые модули. Варианты: - Добавить `#ifndef CHATGUI_ANDROID ... #endif` с заглушками - Вынести проблемный код в `_lite` версию файла - Создать `stubs.c` с пустыми реализациями нужных функций ### 2.3. utun_instance_lite Создать `src/utun_instance_lite.c/h` — облегчённый instance: - Поля: `uasync* ua`, `SC_MYKEYS my_keys`, `sqlite3* db`, `stcp_server* srv`, `void* control_srv` - Нет: tun, etcp_connections, routing, bgp, nat, firewall, proxy ### 2.4. config_parser — упрощение Убрать секции, не нужные в Android-версии (tun, etcp, routing, bgp, nat, firewall, proxy). Оставить: node, control, db, debug. ### 2.5. Проверка ```bash cd tools/chatgui-android/build && make -j4 ``` Все .c компилируются, линкуется libutun_lite.a. --- ## Этап 3. Headless CLI + управляющий сокет **Цель:** Запустить headless-экземпляр, подключиться control-клиентом, выполнить базовые команды. ### 3.1. headless_main.c ```c int main(int argc, char** argv) { // 1. Парсинг аргументов: -c config.cfg [-p port] [-l logfile] // 2. Чтение конфига (config_parser) // 3. Инициализация ключей (генерация если нет) // 4. Открытие БД (sqlite3_open, WAL, создание таблиц) // 5. Создание uasync // 6. Запуск STCP сервера (stcp_server_listen) // 7. Запуск control сокета (headless_control_init) // 8. uasync event loop (uasync_run) // 9. cleanup } ``` ### 3.2. headless_control.c Управляющий TCP-сокет с текстовым протоколом (см. ARCHITECTURE.md §6). Реализация: - `headless_control_init(ua, port)` — создать слушающий сокет - При новом подключении: зарегистрировать fd в uasync, выделить буфер - При данных: парсинг строк, выполнение команд, отправка ответа - Поддержка subscribe: сохранить список подписанных клиентов, рассылать события - Команды первого этапа (минимальный набор): - `status` — показать состояние (node_id, port, uptime) - `connections` — список STCP соединений (заглушка: "none") - `debug` / `debug_level` — управление отладкой - `help` — список команд - `quit` — отключиться ### 3.3. Тестирование этапа 3 ```bash # Терминал 1: запуск сервера ./headless -c test_node1.cfg -p 9999 -l node1.log # Терминал 2: подключение control-клиентом nc localhost 9999 > status node_id: 0xABCD... uptime: 5s connections: 0 > help ... > quit bye ``` **Критерий:** headless запускается, слушает control-порт, отвечает на status/help/debug/quit. ### 3.4. Тестирование — два экземпляра, STCP connect Добавить команды: - `connect [pubkey_hex]` — STCP клиент к другому headless - `connections` — показывает peer после connect ```bash # Терминал 1: node1 (сервер, порт 9999 control, порт 12345 stcp) ./headless -c node1.cfg -p 9999 # Терминал 2: node2 (сервер, порт 9998 control, порт 12346 stcp) ./headless -c node2.cfg -p 9998 # Терминал 3: управляем node2 nc localhost 9998 > connect 127.0.0.1 12345 connected to > connections 1: 127.0.0.1:12345 ready ``` **Критерий:** Два headless устанавливают STCP handshake, соединение активно. ### 3.5. Тестирование — сообщение между двумя узлами Добавить команды: - `create_channel ` — создаёт канал - `send ` — отправляет сообщение подключённому пиру - `messages [limit]` — показывает сообщения из локальной БД ```bash # node1 (control port 9999) > create_channel general created channel general > send general hello from node1 # сообщение сохраняется в БД msg_id=1 > messages general [1] 12:00:00 (me) hello from node1 ``` После того как node2 подключён и канал синхронизирован: ```bash # node2 (control port 9998) > messages general [1] 12:00:00 hello from node1 ``` **Критерий:** Сообщение, отправленное на node1, появляется в БД node2 через STCP. --- ## Этап 4. JNI-прослойка (C → Kotlin) **Цель:** Kotlin-код вызывает функции C-ядра через JNI, C-ядро шлёт коллбэки в Kotlin. ### 4.1. android_jni_bridge.c JNI-функции (вызываются из Kotlin): ```c JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeInit( JNIEnv* env, jobject thiz, jstring configPath, jstring dbPath, jint controlPort); JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDestroy( JNIEnv* env, jobject thiz); JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSendMessage( JNIEnv* env, jobject thiz, jstring channelId, jstring text); JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeCreateChannel( JNIEnv* env, jobject thiz, jstring name, jstring channelId); JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectNode( JNIEnv* env, jobject thiz, jlong nodeId, jstring addr, jint port, jbyteArray pubkey); JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetEventCallback( JNIEnv* env, jobject thiz, jobject callback); ``` Все действия (send, create, connect) отправляются в uasync-поток через `uasync_post`. ### 4.2. Коллбэки (C → Kotlin) При `nativeInit()`: 1. JNI сохраняет `g_callback = env->NewGlobalRef(callback)` 2. `chat_event_set_handler(jni_event_handler)` — регистрирует C-обработчик 3. `jni_event_handler(int type, uint8_t* data, int len)`: - Получает JNIEnv (через `g_jvm->AttachCurrentThread`) - Вызывает `env->CallVoidMethod(g_callback, onEvent, type, byteArray)` ### 4.3. NativeLib.kt ```kotlin object NativeLib { init { System.loadLibrary("utun") } external fun nativeInit(configPath: String, dbPath: String, controlPort: Int): Boolean external fun nativeDestroy() external fun nativeSendMessage(channelId: String, text: String) external fun nativeCreateChannel(name: String, channelId: String) external fun nativeConnectNode(nodeId: Long, addr: String, port: Int, pubkey: ByteArray) external fun nativeSetEventCallback(callback: ChatEventCallback) } interface ChatEventCallback { fun onEvent(type: Int, data: ByteArray?) } ``` ### 4.4. Тестирование (пока без Android) Собрать `libutun.so` под Linux (не под Android NDK) и написать маленькую Java-программу, которая грузит библиотеку и вызывает `nativeInit()`. Или написать C-тест, который имитирует JNI-вызовы напрямую. **Критерий:** `nativeInit()` создаёт uasync, запускает STCP сервер, открывает БД. `nativeSendMessage()` пишет в БД. Коллбэк вызывается при событиях. --- ## Этап 5. Android UI (Jetpack Compose) **Цель:** Минимальное Android-приложение: список каналов, чат, отправка сообщений. ### 5.1. Gradle проект - `app/build.gradle.kts`: compileSdk 34, minSdk 26, NDK 26+ - Зависимости: Compose BOM, Material3, Navigation Compose, Room, Coroutines - NDK: `externalNativeBuild { cmake { path "src/main/cpp/CMakeLists.txt" } }` ### 5.2. app/src/main/cpp/CMakeLists.txt NDK-сборка: компилирует `android_jni_bridge.c` + линкует `libutun_lite` (сборка libutun_lite отдельно или inline в том же CMakeLists.txt). Добавляет OpenSSL из NDK (или prebuilt .so). ### 5.3. Экраны (Compose) **ChannelListScreen:** - LazyColumn каналов (ChannelItem: иконка, имя, последнее сообщение, unread badge) - FAB "+" для создания канала - Pull-to-refresh **ChatScreen:** - TopAppBar с именем канала - LazyColumn сообщений (MessageBubble: аватар, имя, текст, время) - InputBar (TextField + IconButton send) внизу **CreateChannelDialog:** - AlertDialog с TextField имени - Кнопки Create/Cancel ### 5.4. ViewModel ```kotlin class ChatViewModel : ViewModel() { private val _channels = MutableStateFlow>(emptyList()) val channels: StateFlow> = _channels private val _messages = MutableStateFlow>(emptyList()) val messages: StateFlow> = _messages fun init(configPath: String, dbPath: String) { ... } fun selectChannel(channelId: String) { ... } fun sendMessage(text: String) { ... } fun createChannel(name: String) { ... } // Вызывается из JNI callback потока: fun onNativeEvent(type: Int, data: ByteArray?) { ... } } ``` ### 5.5. Room DB (read-only) DAO для чтения channels и messages. Использует ту же БД что и C-ядро (WAL mode). Room открывает БД в read-only, C-ядро пишет. Конфликтов нет. ### 5.6. Тестирование Запустить на эмуляторе или реальном устройстве. Проверить: - Открывается список каналов (пустой) - Создание канала (появляется в списке) - Открытие канала (пустой чат) - Отправка сообщения (появляется в чате) **Критерий:** Приложение запускается, можно создать канал, написать сообщение, оно сохраняется в БД. --- ## Этап 6. Android Headless Service **Цель:** Фоновый сервис без UI — для отладки сетевого стека на реальном устройстве. ### 6.1. HeadlessService.kt ```kotlin class HeadlessService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // Запуск foreground notification // NativeLib.init(configPath, dbPath, controlPort) // Уведомление: "uTun Chat running" return START_STICKY } override fun onDestroy() { NativeLib.destroy() } } ``` ### 6.2. Отладка через adb ```bash # На устройстве adb shell am startservice com.utun.chat/.headless.HeadlessService # Логи adb logcat -s utun:* # Control socket (проброс порта) adb forward tcp:9999 tcp:9999 nc localhost 9999 > status > debug etcp=trace > connect ``` **Критерий:** Сервис запускается, control-сокет доступен через adb forward, логи видны в logcat. --- ## Этап 7. End-to-end тесты ### 7.1. Headless тесты (Linux) Скрипт `test_headless.sh`: 1. Запускает node1 на порту 9999 (control) и 12345 (stcp) 2. Запускает node2 на порту 9998 (control) и 12346 (stcp) 3. Через control-сокет node1: создаёт канал "general" 4. Через control-сокет node1: получает invite-ссылку 5. Через control-сокет node2: подключается к node1 6. Через control-сокет node2: join по invite 7. Через control-сокет node1: send "test message" 8. Через control-сокет node2: messages (проверка что сообщение пришло) 9. Остановка обоих экземпляров ```bash cd tools/chatgui-android && ./test_headless.sh ``` ### 7.2. Android Instrumentation тесты ```kotlin @Test fun testNativeInitAndSend() { NativeLib.init(configPath, dbPath, controlPort) // Проверить что БД создалась val db = Room.databaseBuilder(...) NativeLib.sendMessage("test_ch", "hello") // Подождать и проверить что сообщение в БД } ``` ### 7.3. UI тесты (Compose) ```kotlin @Test fun testCreateChannelAndSend() { composeTestRule.setContent { ChatApp() } // Нажать FAB "+" // Ввести имя канала // Нажать Create // Проверить что канал появился // Открыть канал // Ввести текст // Нажать Send // Проверить что сообщение отобразилось } ``` --- ## Контрольные точки | Этап | Что проверяется | Где тестируется | |------|----------------|-----------------| | 1 | Проект собирается | Linux | | 2 | libutun_lite.a компилируется без ошибок | Linux | | 3 | Headless запускается, control socket отвечает | Linux | | 3.4 | Два headless соединяются по STCP | Linux | | 3.5 | Сообщение доставляется от node1 к node2 | Linux | | 4 | JNI-функции вызываются, коллбэки приходят | Linux / эмулятор | | 5 | Android приложение запускается, UI работает | Эмулятор / устройство | | 6 | Headless Service работает, control через adb | Устройство | | 7 | End-to-end тесты проходят | Linux + эмулятор | ## Правила разработки - **Все C-файлы** — C99, стиль как в AGENTS.md - **Логирование:** каждый модуль логирует инициализацию/очистку и ошибки - **Memory:** u_malloc/u_free из lib/mem.h - **Headless control:** все команды логируются входящие/исходящие - **JNI:** каждая функция проверяет аргументы на NULL, логирует ошибки - **Android UI:** ViewModel использует StateFlow, не держит ссылки на Context - **Тесты:** каждая команда control-сокета проверяется хотя бы одним вызовом