Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .devtools.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ APP_NAME=ezvpn
MAIN_ACTIVITY=dev.flexaccess.ezvpn/.MainActivity
CORE_REPO=ezvpn
CORE_GH_REPO=flexaccessdev/ezvpn

# The app version (ezvpn.versionName/versionCode) moves independently of the
# core; a bump only rewrites the release pin, and the build derives the core
# version shown in the UI from ezvpn.releaseTag.
BUMP_VERSION_MODE=core-key
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
- make changes on the sibling project ../ezvpn (the Rust core this app loads) when needed; its design notes for this app are in ../ezvpn/docs/Android-App.md. Run its `cargo ndk -t arm64-v8a --platform 29 clippy --lib -- -D warnings` after Rust changes touching the Android build.
- always test on the development emulator over adb (`10.22.35.66:5555`, an arm64 Android VM; `adb connect 10.22.35.66`); a VpnService cannot be exercised meaningfully on the JVM. `scripts/run-device.sh` targets it by default (`ADB_SERIAL` overrides), builds the local core, installs, launches, and tails logcat. When several devices are attached, always pass `-s <serial>` to adb / set `ANDROID_SERIAL` for Gradle install tasks. `scrcpy -s 10.22.35.66:5555` mirrors/controls the emulator screen.
- the Android Studio emulator is relay-only by default (its Wi-Fi sits behind netsim's user-mode NAT, and `-vmnet-bridged` alone only re-backs the cellular `eth0`); for LAN addresses / direct iroh paths start it with `-feature -WiFiPacketStream -vmnet-bridged <host iface>` (sudo, Apple silicon) — README "Emulator networking". Verified with emulator 37.1.11 on a stock Play Store image, no root needed.
- the Rust artifact (`libezvpn.so` per ABI, zipped as `libezvpn-android.zip`) is delivered by download + sha256 pin in `gradle.properties` (`app/build.gradle.kts` `fetchEzvpnJniLibs`). Bump with `scripts/bump-jnilibs.sh <tag>` after the ezvpn release workflow publishes the asset. For FFI dev against a local build run `../ezvpn/build-android.sh` then set `EZVPN_LOCAL_JNILIBS=1` for every gradle invocation — only the exact value `1` opts in.
- the Rust artifact (`libezvpn.so` per ABI, zipped as `libezvpn-android.zip`) is delivered by download + sha256 pin in `gradle.properties` (`app/build.gradle.kts` `fetchEzvpnJniLibs`). Bump with `scripts/bump-jnilibs.sh <tag>` after the ezvpn release workflow publishes the asset; it only rewrites the pin — the app's own `ezvpn.versionName`/`ezvpn.versionCode` are independent of the core release and are bumped by hand (the UI shows both, the core one via `BuildConfig.EZVPN_CORE_VERSION`). For FFI dev against a local build run `../ezvpn/build-android.sh` then set `EZVPN_LOCAL_JNILIBS=1` for every gradle invocation — only the exact value `1` opts in.
- `EzvpnNative` must stay at `dev.flexaccess.ezvpn.EzvpnNative`: the JNI symbol names in ../ezvpn/src/ffi_android.rs encode that class. `EzvpnNative.init(context)` must run once before anything else (Application.onCreate): it registers the JVM/context that iroh's Android DNS/interface discovery needs, or the first connect aborts the process.
- pure logic (CIDR math, the route plan with its underlay bypass — `excludeRoute` on API 33+, subtraction below, profile model/validation, split-DNS rules, JSON shapes) lives in `tunnelcore` (no Android deps) so it is unit-testable with `./gradlew :tunnelcore:test`. Put new pure helpers there, not in the service.
- no foreground-service notification: the system binds the VpnService while its interface is established, which keeps the process alive (WireGuard does the same). Don't add one.
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ scripts/build-release-apk.sh --unsigned # no signing (not installable as is)
Google Play only accepts an App Bundle (`.aab`), on every track including
internal testing; `--bundle` builds one signed with the same keystore (Play
App Signing uses it as the upload key). Every upload needs a higher
`ezvpn.versionCode` in `gradle.properties`.
`ezvpn.versionCode` in `gradle.properties`. The app's `ezvpn.versionName` /
`ezvpn.versionCode` are its own and independent of the pinned `ezvpn` release:
`scripts/bump-jnilibs.sh` only re-pins the core, whose version the app shows
next to its own.

The release key is a keystore outside the repo (default
`~/.config/ezvpn-android/release.jks`, override with `EZVPN_KEYSTORE`;
Expand Down
5 changes: 5 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,13 @@ android {
applicationId = "dev.flexaccess.ezvpn"
minSdk = 29
targetSdk = 37
// The app's own version (gradle.properties), independent of the core pin.
versionCode = providers.gradleProperty("ezvpn.versionCode").get().toInt()
versionName = providers.gradleProperty("ezvpn.versionName").get()
// The pinned libezvpn release, shown next to the app version in the UI.
// A local FFI build (EZVPN_LOCAL_JNILIBS) still reports the pinned
// number: the local .so carries none.
buildConfigField("String", "EZVPN_CORE_VERSION", "\"${ezvpnReleaseTag.removePrefix("v")}\"")

// 64-bit only (Google Play's 64-bit requirement; 32-bit devices are
// not supported): arm64-v8a for phones plus x86_64 for VMs/emulators.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ fun TunnelListScreen(
},
bottomBar = {
Text(
stringResource(R.string.tunnel_list_version, BuildConfig.VERSION_NAME),
stringResource(R.string.tunnel_list_version, BuildConfig.VERSION_NAME, BuildConfig.EZVPN_CORE_VERSION),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<!-- Tunnel list -->
<string name="tunnel_list_auth_keys">Auth keys</string>
<string name="tunnel_list_add_profile">Add profile</string>
<string name="tunnel_list_version">ezvpn %1$s</string>
<string name="tunnel_list_version">ezvpn %1$s · core %2$s</string>
<string name="tunnel_list_empty_title">No profiles</string>
<string name="tunnel_list_empty_hint">Tap + to add a VPN profile.</string>
<string name="tunnel_list_waiting">Waiting…</string>
Expand Down
11 changes: 7 additions & 4 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ android.nonTransitiveRClass=true
# The Rust core (libezvpn.so, built from the sibling ../ezvpn repo by its
# build-android.sh and released as libezvpn-android.zip). By default the app
# downloads this pinned release zip and verifies its sha256 (reproducible).
# Bump both together with scripts/bump-jnilibs.sh <tag>, which also sets the
# app versionName below. For local FFI dev set EZVPN_LOCAL_JNILIBS=1 to use
# ../ezvpn/dist/android/jniLibs instead (see README).
# Bump both together with scripts/bump-jnilibs.sh <tag>. The tag (minus "v")
# is also what the UI shows as the core version (BuildConfig.EZVPN_CORE_VERSION).
# For local FFI dev set EZVPN_LOCAL_JNILIBS=1 to use ../ezvpn/dist/android/jniLibs
# instead (see README); the UI still shows the pinned number then.
ezvpn.releaseTag=v0.0.44
ezvpn.releaseSha256=3f636dc29de73f992d7b94f117b1b1eed49d838dfd2cded9986f84764a116a78

# App version; versionName follows the pinned ezvpn release (numeric part).
# The app's own version, independent of the pinned ezvpn release: a core bump
# does not touch these. Bump by hand when the app changes; every Play upload
# needs a higher versionCode.
ezvpn.versionName=0.0.44
ezvpn.versionCode=5
Loading