18 KiB
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. Проверка
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. Проверка
cd tools/chatgui-android/build && make -j4
Все .c компилируются, линкуется libutun_lite.a.
Этап 3. Headless CLI + управляющий сокет
Цель: Запустить headless-экземпляр, подключиться control-клиентом, выполнить базовые команды.
3.1. headless_main.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
# Терминал 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 <addr> <port> [pubkey_hex]— STCP клиент к другому headlessconnections— показывает peer после connect
# Терминал 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 <node1_pubkey_hex>
connected to <node1_id>
> connections
1: <node1_id> 127.0.0.1:12345 ready
Критерий: Два headless устанавливают STCP handshake, соединение активно.
3.5. Тестирование — сообщение между двумя узлами
Добавить команды:
create_channel <name>— создаёт каналsend <channel_id> <text>— отправляет сообщение подключённому пируmessages <channel_id> [limit]— показывает сообщения из локальной БД
# 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 подключён и канал синхронизирован:
# node2 (control port 9998)
> messages general
[1] 12:00:00 <node1_id> hello from node1
Критерий: Сообщение, отправленное на node1, появляется в БД node2 через STCP.
Этап 4. JNI-прослойка (C → Kotlin)
Цель: Kotlin-код вызывает функции C-ядра через JNI, C-ядро шлёт коллбэки в Kotlin.
4.1. android_jni_bridge.c
JNI-функции (вызываются из Kotlin):
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():
- JNI сохраняет
g_callback = env->NewGlobalRef(callback) chat_event_set_handler(jni_event_handler)— регистрирует C-обработчикjni_event_handler(int type, uint8_t* data, int len):- Получает JNIEnv (через
g_jvm->AttachCurrentThread) - Вызывает
env->CallVoidMethod(g_callback, onEvent, type, byteArray)
- Получает JNIEnv (через
4.3. NativeLib.kt
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
class ChatViewModel : ViewModel() {
private val _channels = MutableStateFlow<List<Channel>>(emptyList())
val channels: StateFlow<List<Channel>> = _channels
private val _messages = MutableStateFlow<List<Message>>(emptyList())
val messages: StateFlow<List<Message>> = _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
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
# На устройстве
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 <addr> <port> <pubkey>
Критерий: Сервис запускается, control-сокет доступен через adb forward, логи видны в logcat.
Этап 7. End-to-end тесты
7.1. Headless тесты (Linux)
Скрипт test_headless.sh:
- Запускает node1 на порту 9999 (control) и 12345 (stcp)
- Запускает node2 на порту 9998 (control) и 12346 (stcp)
- Через control-сокет node1: создаёт канал "general"
- Через control-сокет node1: получает invite-ссылку
- Через control-сокет node2: подключается к node1
- Через control-сокет node2: join по invite
- Через control-сокет node1: send "test message"
- Через control-сокет node2: messages (проверка что сообщение пришло)
- Остановка обоих экземпляров
cd tools/chatgui-android && ./test_headless.sh
7.2. Android Instrumentation тесты
@Test
fun testNativeInitAndSend() {
NativeLib.init(configPath, dbPath, controlPort)
// Проверить что БД создалась
val db = Room.databaseBuilder(...)
NativeLib.sendMessage("test_ch", "hello")
// Подождать и проверить что сообщение в БД
}
7.3. UI тесты (Compose)
@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-сокета проверяется хотя бы одним вызовом