Files
utun2/tools/chatgui-android/AGENTS.md
T
evgeny 2de554f8b9 call: единый голосовой стек (libutun_voice) + SoundTouch на Android
- voice_jitter (кодированные Opus-кадры + decode-on-pull + SoundTouch time-stretch)
  вместо PCM-буфера audio_jitter; излишек держится до декодирования
- call_audio.c/.h — единая реализация для desktop/Android/headless (C-интерфейс)
- libutun_voice (voice_sources.cmake): SoundTouch + voice_jitter + call_audio + call_tones
- Android: pull-модель (nativeCallAudioPull), убран Kotlin FIFO, ANDROID_STL=c++_static
- headless audio-сокет: PCM-протокол (decode-only, без тактирования), daemon остаётся C-only
- CALL_STATS расширен dropped/underruns; удалены audio_jitter.cpp и libutun_lite/call_audio.c
- тест test_voice_jitter (ёмкость/догон/стационар), test_call_headless переведён на PCM
2026-09-16 20:26:55 +03:00

301 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md — chatgui-android (Android P2P Chat)
Android-версия чатгуи: P2P чат на STCP (TCP), UI на Jetpack Compose (Kotlin).
C-ядро: `libutun_lite` (выборочная компиляция нужных .c из `lib/` и `src/`).
Весь UDP-стек (ETCP, BBR, loadbalancer, NAT, routing, TUN) исключён.
Транспорт: только STCP (X25519 + AES-CCM поверх TCP).
## Состав проекта (5 частей)
```
tools/chatgui-android/
├── libutun_lite/ # C-ядро (статическая библиотека)
│ ├── CMakeLists.txt
│ ├── utun_sources.cmake # список компилируемых .c файлов
│ ├── instance_lite.h/c # жизненный цикл (старт/стоп/рестарт, ключи)
│ ├── utun_config_api.h/c # конфиг-провайдер (Kotlin → C)
│ ├── invite_link_c.h/c # invite-ссылки (encode/decode, совместимы с десктопом)
│ ├── voice_recorder.h/c # запись голосовых (PCM→Opus→канал)
│ └── attachment_sender.h/c # отправка файлов в канал
│
├── headless/ # CLI для Linux (тестирование без Android)
│ ├── CMakeLists.txt
│ ├── headless_main.c # точка входа, uasync event loop
│ └── headless_control.c/h # управляющий TCP-сокет (JSON/text протокол)
│
├── jni_bridge/ # JNI прослойка C ↔ Kotlin
│ └── android_jni_bridge.c/h # C API + JNI-функции (компилятся только для Android)
│
├── app/ # Android приложение (Gradle + NDK + Compose)
│ ├── build.gradle.kts
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── cpp/ # NDK: jni_bridge + libutun_lite → libutun.so
│ │ └── CMakeLists.txt
│ ├── java/com/utun/chat/
│ │ ├── ChatApplication.kt # Application, инициализация
│ │ ├── MainActivity.kt # Compose UI, навигация экранов
│ │ ├── data/
│ │ │ ├── NativeLib.kt # JNI-обёртка
│ │ │ ├── ChatRepository.kt # репозиторий (JNI + JSON)
│ │ │ ├── InviteLink.kt # парсер utun://base64blob
│ │ │ ├── ConfigProvider.kt # конфиг из DataStore
│ │ │ ├── LogManager.kt # лог-менеджер
│ │ │ └── ServerEntry.kt # модели данных
│ │ ├── viewmodel/
│ │ │ └── ChatViewModel.kt # StateFlow для UI
│ │ ├── ui/screens/
│ │ │ ├── ChannelListScreen.kt # список каналов + join/create
│ │ │ ├── ChatScreen.kt # сообщения + ввод
│ │ │ ├── JoinChannelDialog.kt # диалог подключения по invite
│ │ │ ├── QrScanScreen.kt # CameraX + ML Kit QR-сканер
│ │ │ ├── SettingsScreen.kt # настройки
│ │ │ └── LogScreen.kt # лог-просмотр
│ │ ├── ui/components/
│ │ │ └── MessageBubble.kt # бабблы сообщений + InputBar
│ │ └── headless/
│ │ └── HeadlessService.kt # фоновый сервис
│ └── res/
│
└── doc/ # документация
├── AGENTS.md # этот файл
├── ARCHITECTURE.md # подробная архитектура
└── IMPL_PLAN.md # план реализации по этапам
```
## Chat-подсистема (src/chat/)
Все модули из `src/chat/` компилируются в `libutun_lite` через `file(GLOB_RECURSE)`.
Описания каждого модуля — см. корневой `AGENTS.md`, секция «Chat (src/chat/)».
## Технологии
| Слой | Язык | Фреймворк |
|------|------|-----------|
| C-ядро | C99 | OpenSSL, SQLite3 |
| Bridge | C + JNI | Android NDK 29 |
| Android UI | Kotlin | Jetpack Compose, Material3, CameraX, ML Kit |
| Сборка C | CMake 3.16+ | GCC (Linux) / Clang (Android NDK) |
| Сборка APK | Gradle 8.x | AGP 8.x, Kotlin 1.9 |
## Зависимости
### Для сборки APK (Android)
| Компонент | Версия | Установка |
|-----------|--------|-----------|
| JDK | 17+ | `apt install openjdk-17-jdk` |
| CMake | 3.22+ | `apt install cmake` |
| Android SDK | API 36 | через [sdkmanager](https://developer.android.com/studio#command-line-tools-only) или Android Studio |
| Android NDK | 29.0.14206865 | `sdkmanager "ndk;29.0.14206865"` |
| Gradle | 8.5 (wrapper) | `./gradlew` скачает сам |
| Kotlin | 1.9.20 | встроен в Gradle плагин |
| adb | platform-tools | `apt install adb` (для прошивки) |
OpenSSL для APK **предсобран** под `arm64-v8a` и `x86_64`, лежит в `app/src/main/cpp/openssl/`.
Собирать отдельно не нужно.
### Для Headless (Linux, отладка без телефона)
| Компонент | Установка |
|-----------|-----------|
| CMake 3.16+ | `apt install cmake` |
| GCC / Clang | `apt install build-essential` |
| OpenSSL dev | `apt install libssl-dev` |
| pthread | встроен в libc |
### Настройка окружения
Создать `local.properties` в корне `tools/chatgui-android/`:
```properties
sdk.dir=/home/user/Android/Sdk
```
Путь `ndk.dir` можно не указывать — Gradle скачает NDK сам (версия из `ndkVersion` в `app/build.gradle.kts`).
Если NDK уже установлен отдельно:
```properties
ndk.dir=/home/user/Android/Sdk/ndk/29.0.14206865
```
### Установка Android SDK (без Android Studio)
```bash
cd /tmp && wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip
unzip commandlinetools-linux-*.zip -d ~/Android
mkdir -p ~/Android/Sdk/cmdline-tools
mv ~/Android/cmdline-tools ~/Android/Sdk/cmdline-tools/latest
export ANDROID_HOME=~/Android/Sdk
export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$PATH
yes | sdkmanager --licenses
sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;29.0.14206865"
```
## Сборка
```
./build.sh # clean + сборка + прошивка на подключённый телефон
./build.sh noinstall # только сборка, без прошивки
./build.sh <SN> # clean + сборка + прошивка на конкретный девайс
```
### APK (Android)
```bash
cd tools/chatgui-android
export ANDROID_HOME=/home/user/Android/Sdk
./gradlew assembleDebug
# APK: app/build/outputs/apk/debug/app-debug.apk
```
### Headless (Linux, для отладки сетевого стека)
```bash
cd tools/chatgui-android && mkdir -p build && cd build
cmake .. && make -j4
./headless/headless
```
## Прошивка на телефон
```bash
# Проверить доступные устройства
adb devices
# Установить APK
adb -s <serial> install -r app/build/outputs/apk/debug/app-debug.apk
# Смотреть логи
adb logcat -s utun:*
# Control-сокет (при запущенном HeadlessService):
adb forward tcp:9999 tcp:9999
nc localhost 9999
> {"id":1,"cmd":"status"}
> {"id":2,"cmd":"join","link":"utun://..."}
```
## Конфиг
Конфиг хранится в Android DataStore (`ConfigProvider.kt`) и передаётся в C-ядро
при старте как INI-текст через `nativeStart(configText)`.
Формат (генерируется автоматически, пользователь не редактирует):
```ini
[global]
my_node_name=MyDevice
my_public_key=<64 hex chars X25519>
my_private_key=<64 hex chars X25519>
db_path=/data/data/com.utun.chat/files
db_sync_enabled=1
[server:main]
addr=0.0.0.0:<listen_port>
[allowed_keys]
allow_all=yes
[ntp]
enabled=no
[chatserver]
storage_autoload=1
storage_unit_size=10M
storage_total_size=1G
opus_codec_preset=1
compressor_enabled=0
compressor_max_gain_db=25
compressor_rise_rate=10
media_download_max_peers=3
[debug]
console_level=info
# Опционально — UDP-лог
[log_udp]
ip=<ip хоста>
port=9999
```
Ключи (`my_public_key`, `my_private_key`) генерируются автоматически при первом запуске
и сохраняются в `keys.conf` в `db_path`. Имя узла (`my_node_name`) берётся из
настроек DataStore или `Build.MODEL`.
## Логирование через logreceiver (только Android)
Debug-сообщения с телефона дублируются через UDP на приёмник `tools/logreceiver/`.
Запуск (всегда через скрипт, до старта utun):
```bash
tools/logreceiver/logreceiver_restart.sh
```
Скрипт ротирует логи (история 10 запусков: `.log` → `.log.1` → … → `.log.10`),
убивает старый демон и запускает новый на `0.0.0.0:9999`, вывод в `logreceiver_output.log`.
При каждом запуске логи фиксируются в истории и начинается свежий лог.
В конфиге Android:
```ini
log_udp_ip = <ip хоста>
log_udp_port = 9999
```
## Ключевые файлы для доработок
### C-слой
- `libutun_lite/instance_lite.h/c` — Жизненный цикл uTun для Android: запуск/остановка C-ядра в отдельном потоке, перезапуск при смене конфига, генерация и обновление X25519-ключей, health-check
- `jni_bridge/android_jni_bridge.h/c` — JNI-прослойка Kotlin↔C: все операции из UI (отправка сообщений, вход в каналы, голосовые, статус, настройки) и обратные вызовы (логи, события). Здесь же — JNI-функции, компилируемые только для Android
- `libutun_lite/invite_link_c.h/c` — Кодирование и декодирование invite-ссылок `utun://` в бинарный формат. Совместим с десктопной версией
- `libutun_lite/utun_config_api.h/c` — Поставщик конфигурации из Kotlin в C-ядро через callback-интерфейс (get_string, get_int64, get_int)
- `libutun_lite/voice_recorder.h/c` — Запись голосовых сообщений: накопление PCM-сэмплов с компрессором, кодирование в Opus-файл, отправка в канал через chat_core
- `src/call/call_audio.h/c` (+ `voice_jitter.cpp`, `call_tones.c`, SoundTouch) — единый голосовой стек звонка (`libutun_voice`), собирается через `src/call/voice_sources.cmake`; Opus encode (PCM→peer) и decode + адаптивный джиттер-буфер + time-stretch (PCM отдаётся через `nativeCallAudioPull`). Аудио I/O в Kotlin (AudioRecord/AudioTrack)
- `libutun_lite/attachment_sender.h/c` — Отправка файлов в канал: копирование в media-директорию и регистрация через chat_core (media_index → db_sync)
- `libutun_lite/utun_sources.cmake` — Список всех .c файлов, компилируемых в libutun_lite. Новые файлы добавлять сюда
### Kotlin-слой
- `data/NativeLib.kt` — Kotlin-обёртка над C-библиотекой: все вызовы из Kotlin транслируются в JNI-функции
- `data/CallAudioEngine.kt` — Аудио-движок звонка: AudioRecord/AudioTrack, audio-focus, потоки захвата (feed) / воспроизведения (pull из C-стека)
- `data/ChatRepository.kt` — Хранилище данных: буферизация сообщений и каналов между C-ядром и UI через StateFlow
- `data/InviteLink.kt` — Разбор invite-ссылок: извлекает ID канала, публичный ключ, адреса для подключения
- `data/ConfigProvider.kt` — Поставщик конфигурации из Android DataStore в C-ядро через callback-интерфейс
- `data/LogManager.kt` — Сбор и хранение логов из C-ядра через log-callback
- `viewmodel/ChatViewModel.kt` — ViewModel: StateFlow-состояние для UI (список каналов, сообщения, статус)
- `ui/screens/ChannelListScreen.kt` — Главный экран: список каналов, кнопки Join (по invite-ссылке) и Create
- `ui/screens/ChatScreen.kt` — Экран чата: список сообщений + поле ввода
- `ui/screens/JoinChannelDialog.kt` — Диалог подключения к каналу: ввод/вставка invite-ссылки, предпросмотр, кнопка Connect
- `ui/screens/QrScanScreen.kt` — QR-сканер для invite-ссылок (CameraX + ML Kit)
- `MainActivity.kt` — Точка входа Android-приложения: навигация между экранами, QrScan → JoinDialog flow
### Headless (тестирование без телефона)
- `headless/headless_main.c` — Точка входа headless-режима: парсинг аргументов, запуск C-ядра в uasync event loop
- `headless/headless_control.c/h` — Управляющий TCP-сокет: JSON-команды (`status`, `send`, `join`, `subscribe`) и асинхронные события
## Invite-ссылки (механика подключения к каналу)
Формат идентичен десктопной версии:
```
utun:// + base64(
version(1B, 0x01) |
channel_id(8B BE) |
[header(1B: bits0-1=count-1, bits2-5=family_flags) | pubkey(32B) |
addrs*(socketId(1B) | address(4B v4/16B v6) | port(2B BE))]+
)
```
Node ID вычисляется как `SHA256(pubkey)[0:8] & 0x7FFFFFFFFFFFFFFF` (совместимо с `sc_derive_node_id_from_pubkey()`).
## Правила разработки
- Все C-файлы: C99, стиль как в корневом `AGENTS.md` (4 пробела, snake_case, DEBUG_* макросы)
- Все новые C-файлы добавлять в `libutun_lite/utun_sources.cmake`
- Все новые Kotlin-файлы — Compose, Material3, coroutines/StateFlow
- ViewModel не держит ссылки на Context
- Логи в C — через `debug_set_log_hook` → bridge → Kotlin `LogManager`
- Конфиг в C — через `utun_config_provider_t` коллбэки в Kotlin
- Перед коммитом: головная C-сборка + `./gradlew assembleDebug`