You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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 клиент к другому headless
  • connections — показывает 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():

  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

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:

  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. Остановка обоих экземпляров
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-сокета проверяется хотя бы одним вызовом