Skip to content

Версионирование нативного мобильного кода в git (Android overlay)

Дата: 2026-05-31 Статус: проектирование Затрагивает: kontinuum-veil, kontinuum-app, kontinuum-tether (submodules)

Проблема

Tauri 2 генерирует Android-проект в <app>/.../gen/android, который везде в .gitignore. Рукописный нативный слой (Kotlin VpnService, JNI-bridge, правки манифеста), живущий внутри gen/android, не версионируется и теряется при rm -rf gen/android + tauri android init (регенерация после переименования identifier com.continuum.*com.kontinuum.*).

Конкретный инцидент: при починке Android-сборки Veil каталог gen/android был удалён и пересоздан как голая болванка. Рукописный Kotlin-слой split-tunneling (window.VeilVpn bridge) исчез безвозвратно — его не было в git. Симптом в приложении: страница Split tunneling открывается, но показывает «No apps match» вместо списка приложений, потому что window.VeilVpn не инжектируется в WebView.

Существующий обходной путь у kontinuum-appscripts/patch-android.sh (heredoc целого MainActivity.kt + sed-вставки в манифест, запуск вручную) — подвержен той же потере и хрупок.

Цели

  1. Версионировать рукописный нативный код (Android сейчас, iOS позже) в git как реальные файлы (diff/review/IDE), а не sed-патчи.
  2. Механизм переживает регенерацию gen/ (tauri android init).
  3. Единый подход для трёх приложений и двух платформ; нативный слой будет расти.
  4. Сгенерированный мусор (gradle-wrapper jar, build/, кэши) остаётся вне git.
  5. Восстановить утерянный split-tunnel слой Veil.

Не-цели (YAGNI)

  • Не коммитим весь gen/ (бинарники, build-артефакты).
  • Не отказываемся от cargo tauri android build ради отдельного Android Studio проекта.
  • iOS-слой сейчас не пишем — только закладываем структуру native/apple/.
  • Не меняем существующее поведение kontinuum-app APK (только переносим источник правды).
  • Quick Settings tile («шторка») и auto-connect on boot — НЕ в первой итерации. Запуск headless-VPN из шторки (VeilTileService) и по загрузке (VeilBootReceiver) выносятся в отдельную вторую итерацию (см. ниже). В первой итерации восстанавливаем только JNI-инфраструктуру под них (VeilHeadlessNative.kt), чтобы вторая итерация свелась к добавлению tile/boot-классов без переделок.

Подход A: overlay-каталог + sync-скрипт (выбран)

Структура

В каждом приложении, рядом с tauri-проектом (backend/ у app и veil, client/ у tether):

<tauri-root>/native/
  android/
    overlay/                  # зеркалит gen/android/ файл-в-файл
      app/src/main/AndroidManifest.xml
      app/src/main/java/<pkg>/MainActivity.kt
      app/src/main/java/<pkg>/...            # рукописные .kt
      app/build.gradle.kts                   # если нужны правки
    README.md
  apple/                      # будущее, та же схема (overlay/ под gen/apple/)

Инвариант: путь файла внутри overlay/ идентичен пути внутри gen/android/. sync рекурсивно копирует overlay/*gen/android/*, перезаписывая. Различие «чисто рукописный» (нет в генерации) vs «замещающий» (tauri генерит болванку) на уровне механизма не важно — overlay одинаково копирует оба. Для гибридных файлов (AndroidManifest.xml) храним полную желаемую версию (стратегия «целиком замещать», утверждена).

sync-скрипт

scripts/sync-native.sh (один на приложение, рядом с android-envs.sh). Аргумент: android (default) | apple.

Шаги:

  1. Резолв путей: TAURI_ROOT, GEN=<root>/gen/<platform>, OVERLAY=<root>/native/<platform>/overlay.
  2. Если GEN нет → tauri <platform> init (через android-envs.sh).
  3. Проверка рассинхрона identifier: вычислить ожидаемый java-package path из identifier в tauri.conf.json; если соответствующего каталога в GEN нет (симптом «Project directory … does not exist») → бэкап cp -r "$GEN" /tmp/gen-<platform>-backup-$$ затем rm -rf "$GEN" + tauri <platform> init.
  4. rsync -a "$OVERLAY"/ "$GEN"/ — накатить overlay.
  5. Вывести список наложенных файлов.

Скрипт идемпотентен: повторный запуск ничего не ломает.

Интеграция в nx

В project.json каждого приложения добавить таргет sync:android и подключить его к build:android:

jsonc
"sync:android": {
  "executor": "nx:run-commands",
  "options": {
    "command": "bash ../scripts/sync-native.sh android",
    "cwd": "{projectRoot}"
  }
},
"build:android": {
  "options": { "command": "eval \"$(bash ../scripts/android-envs.sh --export)\" && cargo tauri android build --target aarch64", "cwd": "{projectRoot}" },
  "dependsOn": ["^build", "gen:bindings", "sync:android"]
}

(У tether путь cwd/скриптов — client/; уточнить при реализации.)

После этого nx run veil-backend:build:android сам синхронизирует overlay → собирает. patch-android.sh удаляется.

.gitignore

gen/ остаётся ignored. native/ — НЕ ignored (явно проверить, что правило /backend/gen не задевает native/; при необходимости добавить !native/). Бинарные артефакты в overlay не кладём.

Восстановление split-tunnel слоя Veil

Воссоздаём в kontinuum-veil/backend/native/android/overlay/. Опора: Rust JNI-сигнатуры (backend/src/headless/android.rs, целы) и фронт-контракт.

Контракт window.VeilVpn (13 методов, из frontend)

Split tunnel (entities/splitTunnel/useSplitTunnelStore.ts):

  • listInstalledApps(): string — JSON [{package,label,system}] через PackageManager (требует QUERY_ALL_PACKAGES).
  • getSplitTunnelMode(): string / setSplitTunnelMode(mode)'off'|'allowlist'|'denylist'.
  • getSplitTunnelPackages(): string / setSplitTunnelPackages(json) — JSON-массив package-имён.

VPN lifecycle (entities/connection/useConnectionStore.ts):

  • startVpn(port, user, pass, serverIp) — поднять VeilVpnService (TUN → SOCKS5 от Rust-бэкенда).
  • stopVpn() / isVpnRunning(): boolean.

Settings (pages/settings/SettingsPage.vue):

  • isLocationEnabled() / enableLocation() / disableLocation().
  • isStartOnBootEnabled() / setStartOnBootEnabled(bool).

Файлы overlay

  • MainActivity.kt — наследник TauriActivity; регистрирует bridge в WebView: webView.addJavascriptInterface(VeilBridge(this), "VeilVpn"). (Точку получения WebView уточнить — у tauri это onWebViewCreate/аналог.)
  • bridge/VeilBridge.kt@JavascriptInterface-методы (13 шт. выше). Split-tunnel prefs в SharedPreferences. listInstalledApps через PackageManager.
  • VeilVpnService.ktVpnService: строит TUN, применяет split-tunnel (addAllowedApplication/addDisallowedApplication по mode+packages), запускает проксирование через TUN-JNI (backend/src/tun/android.rs) на SOCKS-порт из startVpn.
  • VeilHeadlessNative.ktexternal fun декларации под существующие Java_com_kontinuum_veil_VeilHeadlessNative_* + System.loadLibrary. Восстанавливается в первой итерации как инфраструктура; реальные потребители (tile/boot) приходят во второй итерации. Сейчас его использует только bridge-метод isStartOnBootEnabled/setStartOnBootEnabled (persist в prefs, без фактического boot-receiver).
  • VeilBootReceiver.kt / VeilTileService.kt — start-on-boot и Quick Settings tile. Вторая итерация (см. раздел ниже), в первой итерации НЕ создаются.
  • AndroidManifest.xmlQUERY_ALL_PACKAGES, <service android:name=".VeilVpnService" android:permission="android.permission.BIND_VPN_SERVICE"> с <intent-filter> на android.net.VpnService, нужные permissions; FOREGROUND_SERVICE для VPN-нотификации.
  • app/build.gradle.kts — если нужны доп. зависимости.

Граница UI ↔ натив (важно)

Основной коннект из UI идёт через Tauri-команды (vpn_connect/vpn_connect_auto/vpn_connect_forceget_proxy_credentials), затем JS зовёт VeilVpn.startVpn(...) чтобы поднять TUN. JNI-путь (VeilHeadlessNative) — только для headless (tile/boot). То есть Rust-бэкенд уже даёт SOCKS5; задача нативного слоя — TUN+split-tunnel поверх него и enumeration приложений. Это снижает объём: бизнес-логику VPN заново писать не нужно.

Вторая итерация: Quick Settings tile + start-on-boot

Отдельный последующий этап (не входит в первую итерацию восстановления). Цель — паритет с утерянной версией: поднимать VPN headless (~10ms, без запуска WebView/UI) из «шторки» и по загрузке устройства. Опирается на уже восстановленный в первой итерации VeilHeadlessNative.kt (JNI → HeadlessBackend в Rust).

Состав:

  • VeilTileService.ktTileService: по тапу в шторке вызывает VeilHeadlessNative.initBackend()connectActiveServer() → поднимает VeilVpnService с полученными SOCKS-кредами; отражает состояние (on/off) на плитке.
  • VeilBootReceiver.ktBroadcastReceiver на BOOT_COMPLETED: если isStartOnBootEnabled → тот же headless-путь. Требует, чтобы VPN-разрешение было выдано ранее через UI (система не покажет диалог из boot-broadcast).
  • AndroidManifest.xml — регистрация <service> tile (BIND_QUICK_SETTINGS_TILE) и <receiver> boot (RECEIVE_BOOT_COMPLETED).

Всё это кладётся в тот же native/android/overlay/ и подхватывается sync-механизмом — отдельных изменений в инфраструктуре не требуется.

Миграция kontinuum-app

Перенести содержимое scripts/patch-android.sh в native/android/overlay/ как реальные файлы:

  • MainActivity.kt (storage permissions logic), AndroidManifest.xml (storage perms + requestLegacyExternalStorage + FileProvider). Удалить patch-android.sh. Добавить sync:android + dependsOn. Поведение APK не меняется.

kontinuum-tether

Завести каркас client/native/android/overlay/ (минимальный/пустой + README), sync:android в client/project.json. Рукописного Kotlin сверх tauri сейчас нет (только Rust client/src/android/*.rs) — инфраструктура на вырост.

Сломанные симлинки submodule (сопутствующее)

Внутри kontinuum-veil и kontinuum-tether симлинки scripts/* → ../../continuum-app/... битые (каталог переименован в kontinuum-app). Временный костыль — корневой continuum-app → kontinuum-app (untracked). Правильно: внутри каждого submodule перенацелить симлинки на ../../kontinuum-app/..., закоммитить в submodule, удалить костыль. Включается в план как отдельный шаг.

Тестирование / верификация

  • sync-native.sh идемпотентен: двойной запуск → одинаковый gen/.
  • Сборка: nx run veil-backend:build:android проходит, APK содержит VeilVpnService, QUERY_ALL_PACKAGES, bridge-классы (проверка через unzip/aapt dump).
  • Функционально: установить подписанный APK на устройство BL8800Pro, открыть Split tunneling → список приложений непустой; allow/deny переключение влияет на маршрутизацию (smoke).
  • Регенерация: rm -rf gen/android && nx run veil-backend:build:android → overlay восстанавливает слой, список приложений снова на месте.
  • kontinuum-app: APK после миграции эквивалентен (storage perms + FileProvider присутствуют).

Риски

  • Апгрейд Tauri может изменить структуру/болванку генерируемых файлов (MainActivity, manifest) → overlay-версия разойдётся. Митигировать: при апгрейде сверять overlay с свежей генерацией (git-diff), README фиксирует это требование.
  • Точка инъекции bridge в WebView зависит от внутренностей tauri (как получить WebView из TauriActivity). Уточнить в реализации (возможно через onWebViewCreate или сгенерированный RustWebView).
  • Split-tunnel поверх VpnService — самая нетривиальная часть; требует корректного построения TUN и связки с TUN-JNI. Достоверность функциональной части подтверждается тем, что слой уже существовал и работал (скриншот пользователя).