Skip to content

Releases

v3.3.1

Choose a tag to compare

@github-actions github-actions released this 08 Nov 15:35
94d2842

The first stable release since 3.3.0. Previews pre.1-pre.23 were the test bed. CI, release-candidate packaging, signature verification and the x86_64 upgrade check passed; the remaining physical-device regression matrix is tracked in Issue #17.

  • The Dart driver owns the core end to end. Start, stop and reload are serialized; the running core handle is registered with the host, adopted after a Flutter-engine restart, and closed by the service when the UI is gone. A lost core no longer leaves the start button lying or a second box fighting over the same TUN
  • The data plane moves closer to Room. Route profiles are read over dart:ffi with a transparent channel fallback; the schema gate checks the columns every query names, and closed, invalidated or concurrently used SQLite handles now fail safely instead of reading freed memory
  • The notification becomes configurable. A dedicated Notification page exposes title and content templates, speed and traffic switches, the service group and the button picker. Templates render {title}, {up}, {down}, direct and total counters, and multi-line content uses BigText; the pre.17 ICU crash is covered by JVM tests and the Kotlin regex audit
  • The IP card is faster and richer. The quality chain reports each answer as it lands and no longer waits on a silent source; the outbound detail page adds an explicit, on-demand Bettbox-style lookup with merged geography/organisation fields and a bounded fraud-risk bucket
  • Routing tools catch up with Throne. The rule editor gains the upstream scalar pickers, keeps unknown legacy values rather than dropping them, imports sing-box rule fragments and DNS dns.rules fragments, and edits list members one per line. Route summaries and condition tokens follow Throne for Android
  • Memory and startup work use Android's accounting. Process RSS/PSS and the core's own memory are reported separately, startup builds the config while the service comes up, and the pinned dashboard build is 5.4 MB smaller without the bundled color-emoji font
  • UI and core stay aligned with upstream. The Prism sync, Throne core 1.4.1 pin, Flrone identifier unification and Bettbox runtime guards from the preview line are included. The in-app updater now points at this repository
  • Release and repository checks are auditable. The AAR channel surface is measured against a budget, the dashboard is pinned, preview/stable release shapes are validated, and governance checks cover the updater constant, tags, assets and changelog/version agreement
  • Verification: flutter analyze --fatal-infos is clean, 387 Flutter tests pass, both CI jobs are green, and channel_audit, symbol_audit, ffi_regression_test, settings_audit, schema_audit, generated_models_check, kotlin_regex_audit, aar_channel_audit and py_syntax_check pass. The lightweight QR-scanner rewrite (PR #16) is deliberately not part of 3.3.1 until its single-code, multi-frame, denied-permission and back-navigation checks have run on an Android device
  • Verification boundary: CI, the signed release-candidate build, ABI/SHA-256/signature checks and an x86_64 API 35 upgrade test passed. The arm64/TV matrix in docs/device-checklist.md remains tracked in Issue #17 and must be completed immediately after publication; the maintainer explicitly approved this release before those remaining checks were completed

v3.3.1-pre.23

v3.3.1-pre.23 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Nov 06:46

The first stable release since 3.3.0, promoted from the 3.3.1 preview line. The APKs are the ones that passed the v3.3.1-pre.23 checks, and 3.3.0 installs now receive this through the in-app updater: throne-update.json is published alongside the APKs.

Promoted from the preview line. This release keeps the v3.3.1-pre.23 tag and its signed APKs, so the installed version still reads 3.3.1-pre.23, BuildConfig.PREVIEW is true, versionCode is 161023 and the core is 3bbe551d. The APK bytes, signature and SHA256SUMS are unchanged from pre.23. A stable-channel rebuild as v3.3.1 will follow once the CI dependency CDN recovers.

  • The core moves to sing-box 1.4.1: THRONE_CORE_REF goes 36b6bc4 -> 3bbe551d (36 commits), after the self-owned throne_* bridge was checked against the new API surface
  • Route and group data is read over dart:ffi: groups and route_profiles come straight from sager_net.db (Room/WAL) with a transparent channel fallback; schema_audit.py column-checks every DAO SELECT against the exported Room schemas
  • The core is never left unowned: lifecycle commands are serialized, a failed start stops the foreground service and rolls the hand-over back, and an FFI start registers its Instance handle with the host so a stop after cleanUpFlutterEngine closes it through Core.closeInstance; a new engine adopts the still-running core over ffi.mode instead of building a second box on the same tun
  • The IP card and memory panel tell the truth: each quality answer is shown as it lands, a source that cannot resolve no longer holds the card for 48 s, and the headline is the process's own PSS with the parts adding up instead of counting the core twice
  • The rule editor reads like Throne's: Rule / Match / Advanced with Advanced collapsed, five conditions as pickers instead of free text, one font size across dialog titles and row values; notification settings get their own page and the diagnostics feature is gone
  • The APK is 5.4 MB lighter: the dashboard is pinned to d77c2f01 and its color-emoji woff2 stripped (7.9 MB -> 2.5 MB); aar_channel_audit.py scans binary-only channels in the Flutter and ThroneCore AARs against a fixed budget
  • Fixed along the way: the pre.17 startup crash, the notification-buttons dialog crash, a concurrent read into a closed sqlite handle, and the review findings over the lifecycle and data layers
  • Checked: flutter analyze --fatal-infos clean, 387 tests pass, CI green on both jobs, all contract audits green
  • Still needs a device: no Android smoke test ran here. Kill the UI while the VPN is up, stop it from the notification, then reopen the app — the tunnel should close on the stop and come back without a second box fighting over the tun

v3.3.1-pre.22

v3.3.1-pre.22 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Nov 03:16

Preview build for testing only. Published as a GitHub prerelease with no throne-update.json; the in-app updater still offers 3.3.0. Install manually from Releases, and do not use it as a daily driver. This one fixes the two things found on pre.21: a row that stood out on the rules screen, and an IP card that held its result back.

  • The batch-add row reads like its neighbours: Batch add simple rules carried the dialog title's style — 20dp emphasized — while the rows around it (Add rule, Reorder rules, Restore default rules) are 17dp list titles, so one row of the rules section stood out bigger and heavier. It is a list title now, and the dialog keeps the title3 style every dialog on these pages uses
  • The IP card shows each quality answer as it lands: the six-source chain delivered one result at the end, so the type and quality chips sat behind skeleton pills — and on a refresh hid the answer the card already had — until the chain converged. lookupIpQuality now reports the answer it would deliver as it arrives: the first inferred one as soon as a source speaks, then the precise one that replaces it. The chips update in place the moment anything is known, and a running lookup refines what is on the card instead of blanking it. The lookup's own outcome is unchanged — the future still completes with the precise answer or the first inferred one, so cache writes, expiry and the failure cooldown read the same record — and a new ip_quality_test pins the two-stage report
  • Checked: flutter analyze --fatal-infos clean, 383 tests pass, CI green on both jobs. On a device: the rules screen's four rows at one size, and the status card's chips appearing as soon as a source answers and staying put through a refresh

v3.3.1-pre.21

v3.3.1-pre.21 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Nov 02:17

Preview build for testing only. Published as a GitHub prerelease with no throne-update.json; the in-app updater still offers 3.3.0. Install manually from Releases, and do not use it as a daily driver. This one is the font-size fix on the settings rows, and nothing else.

  • A row's value now reads at its label's size. pre.20's rule editor showed the name field at 17dp and the picker values beside it at 11dp bold — ListRow sizes its trailing slot for side marks (caption2 emphasized, the size a chevron wants), while FormRow keeps its value in the title slot and ListItem.options shows its value through a body-size button. Three conventions shared one page, and the value read as a footnote next to its own label. context.listValueStyle is the picker value now — body size, secondary colour — and every hand-built value carries it: the rule editor's type / target / outbound / action / reject-method / strategy rows, the route editor's outbound row, node tuning, auto selector, group settings, profile details, the protocol form's picker rows, the test panel, the memory panel and the egress quality failures. 27 rows across ten files
  • Left as it is: ListItem.input rows (Throne settings, backup) draw their value inside prism's list_item.dart at the side style; moving those needs a second drift patch against the vendored copy, so they stay 11dp bold for now
  • Checked: flutter analyze --fatal-infos clean, 382 tests pass (the parallel backoff flake stayed quiet this round), CI green on both jobs. The acceptance check is visual: install this build, open a saved rule, and the name field and every picker value should read at one size

v3.3.1-pre.20

v3.3.1-pre.20 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Nov 01:34

Preview build for testing only. Published as a GitHub prerelease with no throne-update.json; the in-app updater still offers 3.3.0. Install manually from Releases, and do not use it as a daily driver. This one closes out pre.19's two loose ends — the IP card reports one source, the rule editor folds Advanced away — and then takes a structural pass over the code the previous previews landed. No behaviour change beyond those two UI decisions.

  • The IP card reports the probe that answered: the Bettbox-derived enrichment block is gone — ip_enrichment.dart (284 lines: three detail sources, the field-wise merge, the scamalytics risk score) and its 195-line test are deleted, and the two parallel source rows collapse into one. The card shows whichever probe actually answered, so an unanswered egress probe can no longer sit next to a confident-looking quality row. The automatic six-source quality path is untouched
  • The rule editor reads as Rule / Match / Advanced, with Advanced collapsed by default, the ThroneForAndroid shape. Picker dialog titles and option labels share AppTextStyle, so every SimpleDialog in the page lays its rows out at one size
  • pages/routes.dart becomes five files. It held four pages, the rule-text formatters and the enum tables in 1395 lines — the only page module under lib/pages/ that was not one page. route_texts.dart keeps the tables and the pure formatters (ruleName, outboundToken, routeActionColor, ruleActionText, ruleConditions, ruleRowSubtitle); route_profiles.dart, route_editor.dart, rule_order.dart and rule_editor.dart are one page each, carrying the dialog only it opens (_BatchAddDialog, _MultiChoiceDialog, _RuleSetDialog). widgets.dart exports the five, so no call site changed
  • RuleEditorPage.build is 14 lines instead of 250. Match and Advanced used to sit four levels inside children: [if (custom) ...[if (_advancedOpen) ...[. The rule shape comes from getters now (_isSimple / _isEndpoint / _isCustom / _simpleKind), and build is a FormPage over _ruleSection(), _simpleSections() and _customSections(). Lines indented eight levels or more drop from 83 to 29
  • ruleConditions' scalar branch loses a test that could not fail: raw?.toString().trim() is String is true by type, and the same string was trimmed three times. It is a plain if/else now, same output
  • The kernel lifecycle has one attach path and one teardown path: initialize and the tests' debugAttach carried the same four subscription lines twice, and a failed start nested eight cleanup statements five levels into a catch. Both are _attach / _detach now and the catch is two lines. Four declarations nothing referenced are gone: readCString and the private _cstrPublic behind it (the generated bindings carry their own _cstr), configEncoder, and dnsStrategies — the rule editor's enum table is the one source for strategy values
  • Checked: flutter analyze --fatal-infos clean; 382 tests pass and coverage is 54.69% (floor 40%) on a full run — 15 fewer than pre.19, because the deleted enrichment suite was exactly those 15. A parallel run can trip the backoff test described below, which passes 13/13 with its file alone (three runs). channel_audit, symbol_audit, ffi_regression_test and generated_models_check green; neither side of the bridge changed
  • Still needs a device: no Android smoke test ran here, same as pre.17 and pre.19. Installing this build, starting the VPN, opening a saved rule and folding Advanced back open remains the acceptance check
  • Known, unfixed: kernel_handshake_test.dart is flaky when the suite runs in parallel — a manual start during the recovery backoff rebuilds, not swallows depends on real 5/20/80 ms delays. It passes 5/5 alone and failed 2 of 4 full runs, including one on a pages-only change. Replacing Future.delayed with a fake clock is the fix, and it is test infrastructure, not this run

v3.3.1-pre.19

v3.3.1-pre.19 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Nov 16:40

Preview build for testing only. Published as a GitHub prerelease with no throne-update.json; the in-app updater still offers 3.3.0. Install manually from Releases, and do not use it as a daily driver. This one lines the rule editor up with Throne for Android and adds Bettbox-style, on-demand IP detail and risk lookup to the outbound card.

  • Rule conditions become pickers instead of free text: five fields — ip_version, network, protocol, reject_method, strategy — are now SimpleDialog option rows whose value sets are lifted verbatim from Throne's arrays.xml. ip_version stores 4/6, not the ipv4/ipv6 hints the text box used to accept; network gains icmp; protocol carries the full upstream set; reject_method and strategy keep an empty first "unset" option. Throne's ensureValue is reproduced: a rule saved with a value the picker does not know (an older ipv4, say) has that value appended as a live option rather than dropped, so opening and saving an old rule cannot lose it. Picking the unset option removes the key instead of writing an empty string. action gains hijack-dns, keeps the existing bypass, and uses the same helper
  • Bettbox-style IP detail, on demand: common/ip_enrichment.dart adds a self-contained enrichment layer — three detail sources (ip-api.com, get.geojs.io, api.ip.sb) parsed into a field-wise merge that fills gaps without overwriting, plus scamalytics Fraud Score parsed into low / medium / high / very-high buckets (clamped to 0..100, HTML comments ignored). It is wired into the Outbound IP detail page behind an explicit look-up action: nothing fires on the dashboard's automatic refresh path, so the request-count, URL, wave-timing and pixel-height assertions in ip_quality_test.dart, status_panel_test.dart and status_panel_parity_test.dart are untouched. The automatic quality path stays on its original six-source hedged chain; the Bettbox source list is a switchable, on-demand layer. Source labels pass through as given — Bettbox's Hong Kong / Macao / Taiwan country-code rewriting is deliberately not ported
  • Pinned and checked: pure-function tests lock the rule value sets, ensureValue, unset labels, key removal and a legacy ipv4 render; the enrichment module's tests cover all three detail parsers, field merge, risk buckets, all-sources-failing, a malformed single source, JSON round-trip, region pass-through and cancellation. A first CI run caught one inverted assertion in the new test (the implementation was right); a test-only follow-up commit fixed it. The branch now also parses and analyses locally against Dart 3.13.7 under flutter_lints before CI. Real CI: flutter analyze No issues found, 397 tests passed, coverage 54.75% (floor 40%, lib/common 97.71%)
  • Still needs a device: no Android smoke test ran here. The rule pickers and the on-demand lookup are UI paths, so installing this build, opening a saved rule and running the Outbound IP look-up remains the acceptance check

v3.3.1-pre.18

v3.3.1-pre.18 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Nov 12:26

Preview build for testing only. Published as a GitHub prerelease with no throne-update.json; the in-app updater still offers 3.3.0. Install manually from Releases, and do not use it as a daily driver. This one fixes the pre.17 startup crash: on a real device the VPN service could not start at all.

  • The service starts again. The one-pass placeholder render introduced in the pre.14 review round shipped in pre.17 as Regex("\{([A-Za-z]+)}") — the closing } was never escaped. Desktop java.util.regex accepts a lone }, but Android's Pattern delegates to ICU4C, which rejects it with PatternSyntaxException at PatternNative.compileImpl. Because that val sat in ServiceNotification's companion object, the failure surfaced as ExceptionInInitializerError the moment the class loaded, breaking VpnService.onStartCommand → startProxy before the core ever ran: an installed pre.17 crash-looped six times in six seconds and the service never left service: idle. CI stayed green because app:testOssDebugUnitTest runs on the JVM, where the same literal compiles and matches {up}. Both braces are now escaped, and the render moved into NotificationTemplate, a plain Kotlin object with no Android dependency, so a broken template can degrade the notification text without taking the VPN service down with it
  • Two gates pin the gap. Six JUnit tests lock the render contract (all seven built-in keys, unknown keys left as written, a value containing {up} not re-substituted, the ICU-legal pattern itself), and tools/diagnostics/kotlin_regex_audit.py runs in CI to reject an unescaped standalone } in any Regex("…") literal while leaving legal {n} quantifiers alone (nine self-tests; 52 existing literals pass clean). Neither substitutes for a device: only ICU on Android reproduces the difference, so installing this build and starting the VPN remains the acceptance check

v3.3.1-pre.17

v3.3.1-pre.17 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Nov 10:10

Preview build for testing only. Published as a GitHub prerelease with no throne-update.json; the in-app updater still offers 3.3.0. Install manually from Releases, and do not use it as a daily driver. This one is a user-requested UI round: the notification rows get a page of their own, the diagnostics feature goes away, and the route screens read like Throne's.

  • Notification settings move up a level: the seven service-notification rows (group name, proxy/direct speed, traffic total, the two templates, the button picker) leave the General page for their own Notification entry on the settings list, one tap from the drawer like every other page. The General page keeps the connection and behaviour rows
  • The diagnostics feature is deleted: the settings entry, the network-quality and STUN pages and the domain-resolution page are gone — and the whole chain behind them, because none of it had another consumer. Dart loses dnsResolve on the box, the driver's resolveDomains and its short-lived lookup isolate; Kotlin loses the dns.resolve handler, the dnsResolve capability, BboxResolve and DomainResolver, the AIDL's resolveDomains method and ICoreService's override. The C ABI's Mobile.resolveDomains stays in the core patch untouched (the throne_* surface is not the UI's to prune); docs/bbox-bridge.md records the removal. The dashboard's IP-quality card is unaffected — it never ran through the diagnostics screens
  • The route screens read like Throne's (RouteTexts.kt / layout_route_rule_item): a profile row now summarises as Default: proxy · 3 rules instead of Remote · 3 rules; a rule row carries Throne's two lines — the action on top in the route palette (route → direct blue, direct green, reject (drop) red, the config purple, night variants lifted) and the conditions under it in the desktop's token language (suffix: google.com +2 · rule-set: geosite-ir, first value plus a count, same field order as upstream). Both wire shapes grow defaultOutbound so the summary is identical from FFI and channel

v3.3.1-pre.13

v3.3.1-pre.13 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Nov 06:32

Preview build for testing only. Published as a GitHub prerelease with no throne-update.json; the in-app updater still offers 3.3.0. Install manually from Releases, and do not use it as a daily driver. This one fixes the notification buttons dialog that pre.12 shipped broken, and turns the notification text into something the user writes.

  • The notification buttons dialog no longer crashes on open. The in-house AlertDialog renders its body on a Surface, not a Material, so CheckboxListTile's ListTile reached Material.of(context)! and threw "Null check operator used on a null value" (release builds skip the debugCheckHasMaterial assert, so it surfaced as that null check rather than the assert message). The picker's body is now wrapped in a transparent Material
  • The General page gains a Show proxy speed switch (showProxySpeed, default on). Off hides the proxy rate line; the cumulative total under it stays governed by Show traffic total, so the notification can be title-only, title + total, or title + rate
  • The title prefix becomes a title template (notificationTitleTemplate): {title} is the profile name (or "Switching…") and everything else is literal, so 🚀 {title} reads 🚀 Tokyo 01. An existing notificationTitlePrefix is folded in on read as <prefix> {title}, so upgrading users keep their prefix without re-entering it
  • A new notification content template (notificationContentTemplate) replaces the built-in speed line when set. Placeholders: {title}, {up}, {down}, {directUp}, {directDown}, {totalUp}, {totalDown}; a multi-line template uses the expanded (BigText) style. Empty keeps the built-in line and the switches above, so the two ways of shaping the body never fight
  • Both templates and the switch ride the same settings.set channel as the other notification rows, apply on the service's next update (no restart badge), and are covered by the settings audit, the i18n gate (zh entries included) and widget tests

v3.3.1-pre.12

v3.3.1-pre.12 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Nov 05:47

Preview build for testing only. Published as a GitHub prerelease with no throne-update.json; the in-app updater still offers 3.3.0. Install manually from Releases, and do not use it as a daily driver. This one is the structure-audit round: the follow-ups the link-by-link review of the #19 data path asked for, with no feature change. CI is green, but the physical-device checklist (docs/device-checklist.md) is still open — it now has a data-layer section with the exact commands.

The review found three structural risks (a schema gate that only compared version numbers, a versionCode scheme previews can never come back from, and SQLite reads on the UI isolate) and a tail of smaller ones. This lands the ones that did not need a device in the loop, plus the notification customisation round:

  • The service notification gains three customisation rows on the General page: a title prefix (notificationTitlePrefix, prepended to every title — profile name and "Switching…" alike, empty = off), a cumulative traffic switch (showTrafficTotal, the up/down total under the rate line), and a button picker (notificationActions) — the five buttons (stop / previous / next / switch / reset) were already stored and read by the service but had no UI to change them; the picker shows the first three of the enabled ones in the host's fixed order, and an empty selection means no buttons. All three apply on the service's next notification update, so none raises the restart badge; all three ride the same settings.set channel and are covered by the settings audit and the i18n gate (zh entries included)
  • schema_audit.py now parses Room's exported schema JSON and column-checks every SELECT in flutter/lib/data/*_dao.dart — a migration that renames or drops a column the SQL mentions by name fails the push instead of dying silently behind the channel fallback (previously only the version number was compared, and the version staying at 19 was exactly the case nobody would catch)
  • Preview tags are capped at -pre.998 in release.yml: 999 is the stable slot, and a -pre.999 would have built the same versionCode as the stable release of that version; README now states the one-way rule (a preview never installs over the stable release it belongs to — INSTALL_FAILED_VERSION_DOWNGRADE by design)
  • data.path no longer opens (and migrates) Room's database on the main thread — a new respondIo wrapper runs the block on Dispatchers.IO and replies on the main thread; settings.get moves with it (a cold read loads its JSON document from disk)
  • A failed SQLite attach is now classified: a schema version mismatch is permanent for this run, while a cold-start data.path race or a locked file is retried up to three reads and gets a fresh budget every time the app resumes; invalidate() re-attaches from scratch for flows that replace the database file
  • The read connection's busy_timeout drops from 2 s to 250 ms: until the connection moves to a DB isolate, that wait runs on the UI isolate, and anything longer falls back to the channel — which answers off the main thread anyway
  • readOnly now defaults to true in both openAt and openDatabase (every production caller already passed it; only the defaults and one comment disagreed), the speed-test defaults are https:// (both endpoints verified to serve TLS), and TrustAllManager carries the rule that every call site stays behind net_insecure
  • The coverage gate can hold per-directory floors (--min-dir lib/data:NN) and prints a per-directory report; the locale audit runs on every push as a non-blocking report (885 known gaps, now visible instead of invisible)
  • docs/device-checklist.md gains the data-layer section: overwrite-install failure codes, post-migration column drift, WAL concurrency and pulling the database off a debug package to confirm PRAGMA user_version
  • New widget tests cover the three notification rows: the switch and the prefix write through settings.set without the restart badge, and the button picker saves in the host's fixed order regardless of the order the checkboxes were touched in

BuildConfig.VERSION_CODE was also re-checked statically: the updater filters candidates by version name first (Semver.compare(version, installed) > 0), so a device on this preview is never offered 3.3.0 regardless of how AGP fills the code. Still open, by choice: the DB isolate migration (the review's P0-1 — it waits for real-device numbers from the checklist above), the one cleartext IP probe source (http://ip-api.com/json, a user decision), and the missing 10.json schema export (pre-dates the repository).