L×Box v1.4.0
Android VPN client powered by sing-box.
Major release: unified routing rules model, local-only SRS, Stats tabs + Top apps, Debug API, VPN reliability overhaul, per-server detour toggles, performance pass, correctness fixes.
✨ Highlights
- Unified routing rules —
AppRule+SelectableRuletoggles +CustomRuleслиты в одинCustomRule(spec 030). Один редактор, все поля (domain/IP/port/package/protocol/private-IP/srs) в одной форме. - SRS — только локально — sing-box больше ничего не качает сам. Ручной download через ☁ в UI, никаких скрытых auto-update (spec 011).
- Presets = каталог — вкладка Presets превратилась в read-only каталог пресетов, кнопка "Copy to Rules" клонирует пресет в твой реестр.
- Reorder / context menus — правила перетаскиваются за drag-handle, long-press → Delete с подтверждением, long-press на ☁ в редакторе → Refresh SRS / Clear cached file.
- JSON preview — в редакторе правила вкладка View показывает готовый sing-box фрагмент конфига (rule_set + routing rule) + warnings.
- Stats redesign — Statistics-экран получил табы
Overview/Connections(больше не нужен отдельный navigate), карточка Top apps с иконкой + display name + packageName + byte counters, карточка By routing rule, чип sing-box memory. - Template vars UX — формы в Settings / Routing перерисованы: label сверху, описание во всю ширину, поле — тоже.
Test URL/Test interval/Tolerance (ms)получили preset-дропдауны. URLTest interval default поднят с1mдо5mпод invariant spam-avoidance. - Debug API — локальный HTTP-сервер для dev-introspection/control (spec 031):
/state,/clash/*(proxy с auto-auth),/action/*(триггеры пинга/urltest/rebuild'а),/logs,/config,/files/*,/device. Включается runtime-toggle'ом в App Settings → Developer. - Status sync on reattach — при возврате в приложение со свёрнутого/кильнутого процесса (keep-on-exit + VPN активен) UI больше не застревает в Disconnected.
BoxVpnClient.getVpnStatuspull'ит текущее состояние у native-сервиса вHomeController.init. - Auto-update subscriptions toggle — глобальный выключатель в App Settings → Subscriptions + дубль в
SubscriptionsScreenPopupMenu (три точки). Default ON. Off → автоматические триггеры (appStart / vpnConnected / periodic / vpnStopped) скипаются; ручное ⟳ работает всегда (spec 027). - Clash API reference — docs/api/clash-api-reference.md — полный разбор эндпоинтов, полей connections[].metadata, выключенных/недокументированных возможностей sing-box 1.12.12.
- Background / Battery — App Settings → Battery optimization whitelist status + App info (OEM toggles) с hint-диалогом (spec 022).
- Auto-ping after connect — через 5s после подключения VPN пингуем ноды активной группы автоматом (по умолчанию ON).
- ✨auto rename —
auto-proxy-outтег переименован в✨autoс Icons.speed в UI (единая константаkAutoOutboundTag).
🔧 Unified CustomRule model
Три параллельных механизма слиты в один. До v1.4.0:
AppRule— только per-package.SelectableRule— template пресеты, toggle'ятся в Routing → Presets.CustomRule(v1.3.x) — enum type per rule, один matcher.
Теперь один CustomRule имеет все match-поля сразу:
class CustomRule {
CustomRuleKind kind; // inline | srs
List<String> domains, domainSuffixes, domainKeywords, ipCidrs;
List<String> ports, portRanges;
List<String> packages; // package_name (AND с headless match)
List<String> protocols; // routing-rule level (tls/quic/bittorrent/…)
bool ipIsPrivate; // routing-rule level (ORed with rule_set)
String srsUrl; // kind=srs only
String target; // outbound tag OR "reject"
}Semantics per sing-box default rule formula — OR within category (domain-family, port-family), AND between categories. protocol и ip_is_private эмитятся на routing-rule level (headless rule их не поддерживает).
Один редактор с табами Params / View. Params сгруппирован: APPS (над Source) → Source (inline/srs) → MATCH / RULE-SET URL → PORT → PROTOCOL → Delete. Dirty-aware save (подсветка), unsaved back → "Discard changes?" диалог.
🔧 SRS local-only
Убран type:"remote" + update_interval:"24h" из генерации конфига. Все .srs rule_set'ы хранятся локально в $documents/rule_sets/<rule.id>.srs, эмитятся как type:"local", path:…. Скачивание — только ручное:
- Tile cloud icon — ☁ (not cached) / ✅ (cached, green) / ❌ (failed) / spinner. Tap = download/retry.
- Enable gate — switch правила disabled пока нет cached файла.
- Cleanup — Delete rule удаляет cached файл, URL change на save стирает старый кэш.
- Long-press на ☁ в editor → menu: Refresh SRS / Clear cached file.
Причина: провайдеры банят за бездумные авто-запросы, юзер должен контролировать когда что обновляется (см. invariant feedback_no_unplanned_autoupdates).
🔧 Migration (one-shot)
- AppRule → CustomRule.packages —
SettingsStorage._absorbLegacyAppRulesподхватываетapp_rulesключ при первомgetCustomRules, конвертит, удаляет legacy key. - enabled_rules + rule_outbounds → CustomRule —
RoutingScreen._migrateLegacyPresetsпри первой load'е (флагpresets_migrated) конвертит enabled SelectableRule'ы черезselectableRuleToCustom. Fresh installs получают seed изtemplate.selectableRules.where(r => r.defaultEnabled).
Конвертер поддерживает все формы preset'ов:
rule_set:[remote SRS]→kind=srsrule.rule_set:"<tag>"— разворачивает template inline rule_set в match-поля- inline поля прямо в
rule(включаяip_is_private,protocol) — копируются as-is
✨ Routing UI overhaul
3 табы вместо 4
Routing → Channels | Presets | Rules
- Channels — proxy groups +
route.finalselector (без изменений). - Presets — read-only каталог пресетов с кнопкой "Copy to Rules". Для srs-пресетов копия создаётся в disabled-состоянии (юзер должен скачать файл сначала).
- Rules — реестр
CustomRule. ReorderableListView с drag-handle слева, tile edge-to-edge, long-press → Delete, OutboundPicker inline.
Tile дизайн
┌────────────────────────────────────────────────┐
│ ║ ⬤─ Firefox RU domains ☁ ⌄ direct │
│ ║ 2 suffix · 1 app │
└────────────────────────────────────────────────┘
║— drag handle (reorder).- Switch — включатель (disabled для srs без кэша).
- Имя + summary (2 строки) — tap = edit.
- ☁ — для srs only (download status + tap = refresh).
- OutboundPicker справа — inline смена target'а без открытия editor'а.
Editor — Params / View tabs
Params — все match-поля сразу, грамматически OR/AND per sing-box формуле.
View — готовый JSON preview (rule_set + routing rule) через тот же applyCustomRules, что реально применяется при build'е конфига. Copy button + warnings (например "SRS rule X skipped: no cached file").
🔧 Background / Battery
Новая секция в App Settings:
- Battery optimization — status tile (зелёный/красный), tap открывает системную страницу battery-optimization. Fallback на direct-prompt
ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONSесли OEM не поддерживает primary. - App info (OEM power settings) — hint-dialog перечисляет что искать (Autostart, Background activity, Battery, Saver exceptions), потом открывает
ACTION_APPLICATION_DETAILS_SETTINGSс package URI. - Permission
REQUEST_IGNORE_BATTERY_OPTIMIZATIONSдобавлен в AndroidManifest.
🔧 Auto-ping after connect
Через 5s после перехода в connected, HomeController пингует ноды активной группы однократно. Дефолт ON. Отменяется pending timer при disconnect/revoked чтобы не стрельнуть в уже отключённом состоянии. Toggle в App Settings → Feedback.
✨ Stats redesign
StatsScreenтеперьDefaultTabController(length: 2)с вкладкамиOverview/Connections. Вкладка Connections —ConnectionsView, тот же самый компонент что раньше был отдельным экраном, теперь embeddable (без Scaffold/AppBar).- Top apps card — топ-10 приложений по суммарному трафику с иконкой + display name + packageName (монospace subtitle) +
N conns ↑↑ ↓↓. Lazy-cacheAppInfoCacheдёргает nativegetAppInfoper-package,AnimatedBuilderперерисовывает строку когда ответ пришёл. - By routing rule card — распределение активных соединений по rule+rulePayload с процентной полоской.
- Memory chip —
sing-boxрасход RAM в 4-чиповом топ-карде (из/connections.memoryполя Clash API). - Удалена dead навигация: Connections-чип больше не кликабельный (вместо этого — вкладка).
✨ Template vars UX
Формы шаблонных переменных (routing screen Auto Proxy, settings_screen core vars) перерисованы:
- Label сверху — больше нет
ListTileс label/subtitle слева и узким input'ом справа. На узких экранахTest URLраньше ломался наTest/URL(две строки), описание сжималось до 13 символов в строку. - Поле на всю ширину —
Column(crossAxisAlignment: stretch), field растягивается, suffix ▾ прижат справа. EnumDropdownButtonполучилisExpanded: true. - Presets для URLTest —
urltest_intervalдропдаун:30s / 1m / 3m / 5m / 10m / 30m.urltest_tolerance:10 / 30 / 50 / 100 / 200. - Interval default
1m→5m— согласуется с invariantfeedback_subscription_no_spam/feedback_no_unplanned_autoupdates. Юзеры без ручного override получают 5m (меньше фонового трафика).
✨ Debug API (spec 031)
Локальный HTTP-сервер, bind'ится на 127.0.0.1:9269, авторизация Bearer-токеном из App Settings → Developer. Позволяет с хоста по adb forward tcp:9269 tcp:9269 читать state и триггерить действия без повторных сборок APK.
/state,/state/clash,/state/subs,/state/rules,/state/storage,/state/vpn/device— Android version / model / ABI / app version / VPN permission / network type / uptime/config— raw sing-box JSON;/config/pretty— indent'ed/logs— AppLog entries;POST /logs/clear/clash/*— прозрачный проxy с подмешанным Clash secret'ом:/clash/proxies,/clash/group/<tag>/delay,/clash/connections,PUT /clash/proxies/<tag>selector-switch/action/*— триггеры контроллеров:ping-all,ping-node,run-urltest,switch-node,rebuild-config,refresh-subs,download-srs,toast/files/*— read-only доступ к cached SRS + whitelisted external-файлам- Безопасность — host-check middleware (защита от DNS rebinding), токен нигде не пишется кроме internal shared_prefs, bind строго на loopback.
Toggle default OFF, в релизных APK ничего не слушает пока юзер не включит в настройках.
🔧 Keep-on-exit status sync
Фикс: при Keep VPN on exit = true + swipe из recents + возврат в приложение UI показывал Disconnected хотя туннель был активен. И кнопка Start оставалась неактивной.
Причина: native-сервис broadcast'ит BROADCAST_STATUS только на transition (setStatus(new)). Если Flutter-процесс умер, а сервис остался в steady-state Started — новый VpnPlugin.statusReceiver после reattach ничего не получит.
Решение:
BoxVpnService.companion.currentStatus: VpnStatus—@Volatilemirror, обновляется в каждомsetStatus- MethodChannel
getVpnStatus→ возвращаетcurrentStatus.name HomeController.init()сразу после подписки наonStatusChangedpull'ит текущий статус и пропускает через тот же_handleStatusEvent— он эмитит connected/connecting без изменений
🛠 Reconnect reliability overhaul
В v1.4.0 зафиксированы три связанные проблемы в VPN-lifecycle/status pipeline, которые проявлялись как «плашка Config changed — restart VPN to apply не уходит после reconnect», «Stop button иногда игнорируется», «отвалившийся tooltip». Единый ремонт, четыре коммита.
1. Status-sink leak (root cause). BoxVpnClient.onStatusChanged — getter, который на каждый вызов создавал новый receiveBroadcastStream(). Каждый такой вызов вызывал EventChannel.onListen на native, перезаписывая shared statusSink; следующий onCancel (при закрытии короткоживущей подписки типа firstWhere в reconnect) обнулял его. После первого reconnect'а основной listener в HomeController._statusSub становился зомби: Dart думает что подписан, native давно выбросил sink. Все transition events (Starting/Started/Stopping/Stopped) после этого терялись.
Fix: late final _statusStream = ....asBroadcastStream() — один underlying controller на весь lifecycle клиента, native onListen вызывается ровно один раз. Множественные Dart-listener'ы (основной + firstWhere в reconnect) работают параллельно, не перехватывая друг друга.
2. Blocking stopVPN на native. Method channel возвращал true моментально (после отправки broadcast'а), но native выполнял cleanup libbox-ресурсов ещё ~50-80мс. await _vpn.stopVPN() → await _vpn.startVPN() попадал в race: startVPN приходил в onStartCommand guard if (status != Stopped) return START_NOT_STICKY — silent-return без setStatus(Starting) broadcast'а.
Fix: BoxVpnService.stopAwait() возвращает Deferred<Unit>, который completes в setStatus(Stopped) (после полного cleanup'а). Plugin handler — withTimeout(5s).await перед result.success(...). Caller получает control только после реального завершения. Finality-signal через direct Completer — не зависит от доставки broadcast'а (который мы же признали unreliable).
3. Intent-based sticky reset. configStaleSinceStart сбрасывался только в _handleStatusEvent на transition events. Если event потерялся по любой причине (Doze, system kill, platform race) — флаг застревал, плашка висела. Теперь _stopInternal/_startInternal сбрасывают его по факту applied intent'а: stop = «running теперь nothing, saved не stale vs running»; start = «running = saved». Reset в _handleStatusEvent остаётся как defense-in-depth для external transitions (revoke, kill).
4. Reconnect = композиция _stopInternal + _startInternal. Убраны firstWhere, timeout(10s, onTimeout: empty map), координация wait на Dart стороне. Blocking stopVPN гарантирует status=Stopped до startVPN, race исключён на native.
Отчёты: docs/spec/tasks/001-reconnect-sink-leak.md, 002-blocking-stopvpn-intent-reset.md.
🛡 Revoke UX — friendly handling «VPN taken by another app»
Когда другое VPN-приложение захватывает туннель (Hiddify, WireGuard, FortiClient, …), Android вызывает VpnService.onRevoke() на нашем сервисе. Раньше это отражалось красной пилюлей «Revoked by another VPN» в status chip — пугающе и непонятно что делать.
Теперь:
- SnackBar «VPN taken by another app» с быстрой action «Start» на 5 секунд. Тап по Start → наш existing
VpnPlugin.startVpnпуть:VpnService.prepare(занятый слот → non-null intent) → system dialog «VPN connection request» → approve → конкурент получает свой onRevoke → мы занимаем слот. - Status chip показывает обычный Disconnected — видна нейтральная Start кнопка без алармирующего красного. Внутреннее значение
state.tunnel == revokedсохраняется для side-effect detection в listener'е. - Clash endpoint прошлой сессии обнуляется в
_handleStatusEventrevoked-ветке — secret был у убитого sing-box, port мог быть переиспользован. Пересоздаётся на следующемconnectedчерез_refreshClashAfterTunnel.
Отчёт: docs/spec/tasks/003-revoke-ux.md.
🔁 Lifecycle resume re-sync
Event-driven страховка от platform-level потерь broadcast'ов: при возврате в app (AppLifecycleState.resumed) — один getVpnStatus pull, сравнение с Dart state, если divergent — прогон raw status через тот же _handleStatusEvent. Покрывает случаи когда процесс был в background долго, Doze/OOM killer убил service силой без уведомления Flutter и т.п. Если native вернулся с Revoked — сработает тот же snackbar-flow что при live-revoke (P2 выше).
В steady-state ничего не крутится — никакого polling'а/таймеров. Только одноразовый pull при resume.
Отчёт: docs/spec/tasks/004-lifecycle-resume-resync.md.
🔄 Reload button (справа от status chip)
Короткий тап — умный default по состоянию (Connect / Reconnect / Rebuild config + reconnect). Long-press — меню из трёх явных действий: Reconnect, Rebuild config only, Rebuild config + reconnect. Dirty-подсветка сохраняется (primary-container фон когда конфиг изменялся при активном VPN).
Побочный фикс: Flutter Tooltip на Android использует long-press как свой trigger показа тултипа. Это перехватывало long-press GestureRecognizer до нашего InkWell.onLongPress — меню не открывалось. Tooltip заменён на Semantics(label: ...) (accessibility сохранена, жесты не перехватываются).
⚙️ Per-server detour toggles (UserServer)
На экране Node Settings ручного сервера (не подписка) под переключателем «Mark as detour server» (который ставит ⚙ префикс) теперь появляются две дополнительные галки, когда префикс включён:
- Register in VPN groups — показывать detour-сервер в proxy-selector'е. Default OFF.
- Register in auto group — включать в ✨auto urltest polling. Default OFF.
По дефолту detour-сервер скрыт и в selector, и в ✨auto — остаётся доступен только как звено цепочки (через «Detour server» dropdown у других серверов). Это решает давний косяк: ⚙ префикс до 1.4.0 был чисто UI-декорацией, а сервер всё равно попадал в auto и выбирался как обычный endpoint. Для случаев когда detour нужен и как звено, и как самостоятельный endpoint — явные галки-override'ы.
Реализация использует существующий UserServer.detourPolicy (через наследование от ServerList), никаких новых моделей. Builder детектит kDetourTagPrefix в main.tag и применяет per-server политику. Scope — только UserServer (1 server = 1 node в этом проекте); subscription-нод изменение не касается. Детально: docs/spec/tasks/006.
⚡ Performance pass
ConfigCacheвHomeState: парсинг outbound JSON (detourTags+protoByTag) делается один раз приsaveParsedConfig, не на каждый rebuild ListView. С 50+ нодами + сортировкой по ping это убирает заметный jank в node list hot-path'е (раньшеjsonDecodeшёл в itemBuilder'е).sortedNodesmemoize черезlate final— один sort наHomeStateinstance, builder'ы обращаются несколько раз (detour-фильтр + itemCount + itemBuilder) без повторных пересортировок.- Batched
_emitв_handleStatusEvent— раньше 2-3notifyListenersна один status event, теперь один. - Single safety-timer для transient-фазы (Starting/Stopping) — переиспользуемый
Timer?вместо плодящихсяFuture.delayedна каждый transient event. Плюс в timeout-emit добавленconfigStaleSinceStart: false(было пропущено). - Background-paused polling в Stats/Connections screens (
WidgetsBindingObserver): polling Clash API останавливается когда app уходит в background, возобновляется на resume. Экономит battery и method-channel round-trips — раньше 3-секундный polling stats и до 500мс в connections крутились в фоне. - Lint cleanup: unused
dart:typed_dataimport,?protonull-aware marker, docstring escapes.
Детально: docs/spec/tasks/005.
✅ Flutter correctness (P0 code-review fixes)
По результатам deep code review закрыты три анти-паттерна Flutter в home_screen.dart — корректность, не оптимизации.
- Side-effects в
buildубраны. УправлениеAnimationController(_connectingAnim.repeat/stop/reset) жило в_buildStatusChip, вызывающемся изAnimatedBuilder(т.е. из build-фазы). Hot path из heartbeat (каждые 20с) и mass ping (десятки emit/sec) дёргал контроллер анимации лишний раз. Перенесено в listener_onControllerChange, триггерится только при реальной сменеtunnel. Timerне создаётся изbuild. Auto-dismiss таймер дляlastErrorжил вBuilderвнутри build — хрупко при агрессивных rebuild'ах. Перенесён в тот же listener с явным transition detection через_prevError.HomeScreen.dispose()complete. Добавлены_controller.dispose()(отменяет_statusSub, heartbeat, transient timer),_subController.dispose(),_connectingAnim.dispose(). На prod ОС убивала процесс, но hot reload / тесты / потенциальная смена root widget'а давали бы утечку.
Детально: docs/spec/tasks/009 + peer review 008 §A.
📚 Clash API reference
Новый документ docs/api/clash-api-reference.md — полный разбор sing-box 1.12.12 Clash API endpoints: структура /proxies, поля connections[].metadata (включая processPath с uid-суффиксом, dnsMode, rule+rulePayload, chains ordering), /group/<tag>/delay с pitfall'ом "force-urltest не обновляет .now персистентно", /traffic streaming vs snapshot. Используется как reference при доработке clash_api_client и TrafficSnapshot.
✨ AppPicker — lazy icons
Native getInstalledApps больше не возвращает иконки в одном вызове (раньше 500 apps × PNG-compress + base64 = ~10s блокировки UI). Теперь:
getInstalledAppsвозвращает только metadata (pkg/name/isSystem) за сотни msgetAppIcon(pkg)— lazy per-tile запрос, session-level cache_iconCache- Placeholder — CircleAvatar с первой буквой имени, иконка подменяется при arrival'e
- Scroll плавный, иконки появляются по мере viewport'а
Плюс crash-fixes: if (!mounted) return перед setState после awaits; PopupMenu items (Select all / Invert / Import) disabled пока loading; _popped flag — защита от двойного Navigator.pop.
✨ Build speed
Local release build с 38 мин до ~1.5 мин через --target-platform android-arm64 в scripts/build-local-apk.sh. Собираем только одну архитектуру для локального тестирования. CI продолжает собирать все три (arm + arm64 + x64).
🐛 Critical fixes
ip_is_privateunknown field — sing-box отклонял конфиг сip_is_privateв headless rule. Поле не поддерживается в rule_set inline, работает только на routing-rule level. Перенесено, где per sing-box formula становится OR сrule_set.- Protocol-only rules skip'ались — когда в rule только
protocol: [bittorrent](без domain/ip_cidr),matchбыл пустой → skip. Теперь эмитится routing rule без rule_set, всё работает. - AppPicker крэшился при parallel tap — setState без
mountedguard'а + doubleNavigator.pop.
♻️ Rename / refactor
AppRuleкласс полностью удалён; функциональность —CustomRule.packages.applyAppRulesудалён изpost_steps;applyCustomRulesтеперь возвращаетList<String>с warnings.applySelectableRulesудалён — пресеты копируются явно черезselectableRuleToCustom.RuleSetDownloaderпереписан: id-based API (вместо tag), удаленыmaxAge/cacheAll(auto-refresh убран).AppPickerScreen— убран editable title (не нужен внутриCustomRuleEditScreen).IP Filters→Rules(tab rename).
📝 Docs
Обновлены спеки 011 (local ruleset cache — manual-download rewrite), 022 (app settings — battery / background / auto-ping), 030 (полный rewrite — unified model, JSON preview, context menus, migrations). RELEASE_NOTES.md + docs/releases/v1.4.0.md.
Memory invariant feedback_no_unplanned_autoupdates — явная не-функциональность: никаких скрытых background-refresh / TTL-refetch для SRS и прочих ресурсов.
📦 Install
Grab the APK below. Primary target: Android 11+ (API 30+) — fully tested, all features work. Best-effort: Android 8.0–10 — should install and run the basics, but API 30+ features (e.g. silent-kill detection) gracefully no-op. 242/242 tests pass. Release APK ≈ 56 MB (arm64) / 72 MB (universal from CI).
🇷🇺 На русском
Крупный релиз: единая модель routing-правил, локальные SRS без автообновлений, настройки батареи/фона, авто-пинг на connect.
✨ Главное
- Единая модель правил —
AppRule+SelectableRuletoggle'ы +CustomRuleслиты в одинCustomRule. Один редактор — все поля (domain/IP/port/package/protocol/private-IP/srs). - SRS только локально — sing-box ничего сам не качает. Скачивание — ручное по кнопке ☁, никаких скрытых auto-update.
- Presets = каталог — вкладка Presets превратилась в каталог с "Copy to Rules". Копия идёт в твой реестр, там редактируешь/удаляешь как хочешь.
- Reorder / context menus — правила перетаскиваются за drag-handle, long-press → Delete с подтверждением.
- JSON preview — в редакторе вкладка View показывает готовый sing-box фрагмент конфига + warnings.
- Background / Battery — App Settings → Battery optimization status + App info (OEM toggles) с hint-диалогом.
- Auto-ping after connect — через 5s после подключения пингуем ноды активной группы (по умолчанию ON).
- Stats: 2 таба + Top apps —
Overview(трафик, memory, by-rule, top apps) /Connections. Top apps показывает иконку + display name + packageName + счётчик соединений + байты. - Template vars UX — label сверху, поля на всю ширину, preset-дропдауны для URLTest interval/tolerance. Default interval
5m(раньше1m) под invariant spam-avoidance. - Debug API (§031) — локальный HTTP-сервер для dev/staging:
/state,/clash/*,/action/*,/logs. Включается в App Settings → Developer, токен — через Copy в UI. По умолчанию выключен. - Keep-on-exit status sync — при swipe-из-recents + возврате UI больше не застревает в Disconnected.
getVpnStatuspull'ит native-состояние в init'е. - Auto-update подписок toggle — глобальный в App Settings → Subscriptions + дубль в PopupMenu на экране серверов. OFF → автоматические триггеры скипаются, ручное ⟳ всегда работает.
- Clash API reference — docs/api/clash-api-reference.md, полный разбор эндпоинтов и полей.
auto-proxy-out→✨auto— URLTest-тег переименован, Icons.speed в UI.
🔧 CustomRule — один редактор
Три параллельных механизма слиты. Теперь один CustomRule содержит все match-поля сразу, эмит по sing-box default rule formula (OR внутри категории, AND между). protocol и ip_is_private — на routing-rule level (headless их не поддерживает).
Редактор: табы Params / View. Params: APPS (над Source) → Source (inline/srs) → MATCH / URL → PORT → PROTOCOL → Delete. Dirty-aware save, unsaved back → "Discard changes?".
🔧 SRS local-only
Убран type:remote и update_interval из генерации конфига. Файлы в $documents/rule_sets/<id>.srs, в конфиг идёт type:local, path:…. Скачивание только ручное. Switch правила заблокирован пока нет кэша. Delete rule стирает файл, смена URL — тоже.
🔧 Миграции (one-shot)
- AppRule → CustomRule.packages
- enabled_rules + rule_outbounds → CustomRule (с флагом
presets_migrated) - Конвертер preset'ов (
selectableRuleToCustom) поддерживает все формы включаяip_is_privateи template inline rule_set references.
✨ UI routing
- 3 табы: Channels / Presets / Rules
- Tile с drag-handle слева, edge-to-edge, long-press → Delete
- Editor: Params + View, все секции в одном экране
- Cloud-иконка в URL-поле: tap=download, long-press=меню (Refresh / Clear cached file)
🔧 Background / Battery
- Battery optimization tile (зелёный/красный) → открывает системные настройки
- App info с hint-диалогом перед запуском (что искать в OEM-настройках)
- Permission
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
🔧 Auto-ping
Через 5 сек после connected пингуем ноды активной группы одноразово. Отменяется при disconnect. Toggle в App Settings.
✨ AppPicker — lazy icons
Раньше native возвращал иконки всех apps в одном вызове (~10s). Теперь только metadata за сотни ms, иконки lazy per-tile с session-cache'ом.
✨ Build speed
--target-platform android-arm64 в локальном скрипте → release build ~1.5 мин вместо ~38 мин.
🐛 Critical fixes
ip_is_privateперенесён на routing-rule level (в headless его нет, sing-box крашился при старте).- Protocol-only правила теперь эмитятся (раньше skip'ались).
- AppPicker крэш при parallel tap — mounted-guards + pop-guard.
♻️ Rename / refactor
AppRuleудалён (→CustomRule.packages)applyAppRules/applySelectableRulesудаленыRuleSetDownloaderid-based API- IP Filters → Rules (tab rename)
📝 Docs
Спеки 011, 022, 030 переписаны. Memory invariant feedback_no_unplanned_autoupdates.
📦 Установка
Скачай APK ниже. Основная цель: Android 11+ (API 30+) — полностью тестируется, все фичи работают. Best-effort: Android 8.0–10 — базовый функционал должен работать, но фичи API 30+ (например, silent-kill detection) gracefully no-op. 242/242 тестов. Release APK ≈ 56 MB (arm64) / 72 MB (universal).