diff --git a/SHA256SUMS b/SHA256SUMS
index 73e82ce..753a4c0 100644
--- a/SHA256SUMS
+++ b/SHA256SUMS
@@ -2,3 +2,5 @@
35e7dbbb84bbb76b0eea052eff18d58e24c1fa1c43274aa6c1a5c686a1d38f40 *bin/dragontcp-hybrid-server-linux-arm64
2f820ce82a65c285684c875afd0311f1d62afe856f8b878eab682f9864252a19 *bin/dragontcp-hybrid-client-linux-amd64
1b5bcf4d3a446cec397206557fa6f1a4c229b11b8e871a7a1072ac3ae9f81e31 *android/lib/arm64-v8a/libdragontcp_client.so
+8021680a9cff84b0dcdec3c6a1f38c5b6f9620aba2bed89766920e4b2f514afa *android/lib/armeabi-v7a/libdragontcp_client.so
+baa7e4e304f96edd4d1fe8f22b9f11398e1d8ad00e09454afca8419ae718bdd2 *android/lib/x86_64/libdragontcp_client.so
diff --git a/android.zip b/android.zip
new file mode 100644
index 0000000..db6c119
Binary files /dev/null and b/android.zip differ
diff --git a/android/.idea/.gitignore b/android/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/android/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/android/.idea/caches/deviceStreaming.xml b/android/.idea/caches/deviceStreaming.xml
new file mode 100644
index 0000000..4d3d6d9
--- /dev/null
+++ b/android/.idea/caches/deviceStreaming.xml
@@ -0,0 +1,2289 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/.idea/libraries/build.xml b/android/.idea/libraries/build.xml
new file mode 100644
index 0000000..38db4df
--- /dev/null
+++ b/android/.idea/libraries/build.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/.idea/libraries/lib.xml b/android/.idea/libraries/lib.xml
new file mode 100644
index 0000000..d98eb88
--- /dev/null
+++ b/android/.idea/libraries/lib.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml
new file mode 100644
index 0000000..cf90582
--- /dev/null
+++ b/android/.idea/misc.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/.idea/modules.xml b/android/.idea/modules.xml
new file mode 100644
index 0000000..e54e28e
--- /dev/null
+++ b/android/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/.idea/vcs.xml b/android/.idea/vcs.xml
new file mode 100644
index 0000000..6c0b863
--- /dev/null
+++ b/android/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index 07161df..442b324 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -1,12 +1,23 @@
+ android:versionCode="14"
+ android:versionName="14.0-hybrid">
+
+
+
-
+
+
+
+
+
+
+
+
diff --git a/android/android.iml b/android/android.iml
new file mode 100644
index 0000000..b4b3928
--- /dev/null
+++ b/android/android.iml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/build_apk.ps1 b/android/build_apk.ps1
index 83423b3..b30926a 100644
--- a/android/build_apk.ps1
+++ b/android/build_apk.ps1
@@ -125,17 +125,14 @@ $platRoot = Join-Path $SdkRoot 'platforms'
if ($Platform) {
$AJ = Join-Path $platRoot "$Platform\android.jar"
} else {
- $preferred = Join-Path $platRoot 'android-35\android.jar'
- if (Test-Path $preferred) {
- $Platform = 'android-35'; $AJ = $preferred
- } else {
- $p = Get-ChildItem $platRoot -Directory -ErrorAction SilentlyContinue |
- Where-Object { Test-Path (Join-Path $_.FullName 'android.jar') } |
- Sort-Object { [int]($_.Name -replace '\D', '') } -Descending |
- Select-Object -First 1
- if (-not $p) { Die "No android.jar found under $platRoot. Install a platform via the SDK Manager." }
- $Platform = $p.Name; $AJ = Join-Path $p.FullName 'android.jar'
- }
+ # Compile against the newest installed platform: targetSdk 36 must be built
+ # against android-36 or aapt rejects the manifest.
+ $p = Get-ChildItem $platRoot -Directory -ErrorAction SilentlyContinue |
+ Where-Object { Test-Path (Join-Path $_.FullName 'android.jar') } |
+ Sort-Object { [int]($_.Name -replace '\D', '') } -Descending |
+ Select-Object -First 1
+ if (-not $p) { Die "No android.jar found under $platRoot. Install a platform via the SDK Manager." }
+ $Platform = $p.Name; $AJ = Join-Path $p.FullName 'android.jar'
}
if (-not (Test-Path $AJ)) { Die "Android platform jar not found: $AJ" }
@@ -232,7 +229,7 @@ if ($BuildCore -or -not (Test-Path $so)) {
$coreScript = Join-Path $RepoRoot 'build_core.ps1'
if (-not (Test-Path $coreScript)) { Die "$why but $coreScript was not found. Pass -RepoRoot." }
Step 'Native core (Go)...'
- & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $coreScript -ClientOnly -AndroidLibDir $libDir
+ & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $coreScript -ClientOnly -AndroidLibRoot (Join-Path $Root 'lib')
if ($LASTEXITCODE -ne 0) { Die 'build_core.ps1 failed.' }
}
if (-not (Test-Path $so)) { Die "Missing $so - build the Go core first." }
@@ -257,6 +254,9 @@ Invoke-Tool (Join-Path $BT 'aapt.exe') @(
'-S', (Join-Path $Root 'res'),
'-A', (Join-Path $Root 'assets'),
'-I', $AJ,
+ # From targetSdk 30 Android refuses to install an APK whose resources.arsc
+ # is compressed, so tell aapt to store it instead.
+ '-0', 'arsc',
'-F', "$B\resources.ap_"
) 'aapt'
@@ -284,7 +284,7 @@ Invoke-Tool $jar @('cf', "$B\jclasses.jar", '-C', "$B\jclasses", '.') 'jar (java
Step 'DEX...'
Invoke-Tool (Join-Path $BT 'd8.bat') (@(
- '--lib', $AJ, '--min-api', '29', '--output', "$B\dex",
+ '--lib', $AJ, '--min-api', '24', '--output', "$B\dex",
"$B\kclasses.jar", "$B\jclasses.jar", $coro
) + $stdlib) 'd8'
diff --git a/android/lib/armeabi-v7a/libdragontcp_client.so b/android/lib/armeabi-v7a/libdragontcp_client.so
new file mode 100644
index 0000000..370d4c1
Binary files /dev/null and b/android/lib/armeabi-v7a/libdragontcp_client.so differ
diff --git a/android/lib/x86_64/libdragontcp_client.so b/android/lib/x86_64/libdragontcp_client.so
new file mode 100644
index 0000000..5452868
Binary files /dev/null and b/android/lib/x86_64/libdragontcp_client.so differ
diff --git a/android/src/com/dragontcp/client/DragonService.java b/android/src/com/dragontcp/client/DragonService.java
index 13404bf..6396ea7 100644
--- a/android/src/com/dragontcp/client/DragonService.java
+++ b/android/src/com/dragontcp/client/DragonService.java
@@ -361,9 +361,10 @@ public class DragonService extends VpnService {
if (process != null) {
try {
process.destroy();
- if (!process.waitFor(1200, TimeUnit.MILLISECONDS)) {
+ // Process.waitFor(long, TimeUnit) is API 26; minSdk is 24.
+ if (!awaitExit(process, 1200)) {
process.destroyForcibly();
- process.waitFor(800, TimeUnit.MILLISECONDS);
+ awaitExit(process, 800);
}
} catch (Throwable ignored) {
try { process.destroyForcibly(); } catch (Throwable ignored2) {}
@@ -371,6 +372,25 @@ public class DragonService extends VpnService {
}
}
+ /** Waits up to timeoutMs for the process to exit, without needing API 26. */
+ private static boolean awaitExit(Process process, long timeoutMs) throws InterruptedException {
+ long deadline = System.currentTimeMillis() + timeoutMs;
+ while (System.currentTimeMillis() < deadline) {
+ try {
+ process.exitValue();
+ return true;
+ } catch (IllegalThreadStateException stillRunning) {
+ Thread.sleep(50);
+ }
+ }
+ try {
+ process.exitValue();
+ return true;
+ } catch (IllegalThreadStateException stillRunning) {
+ return false;
+ }
+ }
+
private void stopEverythingLocked(String logMessage) {
if (stopping) return;
stopping = true;
diff --git a/android/src/com/dragontcp/client/LogActivity.java b/android/src/com/dragontcp/client/LogActivity.java
index 1c98e82..9b4d247 100644
--- a/android/src/com/dragontcp/client/LogActivity.java
+++ b/android/src/com/dragontcp/client/LogActivity.java
@@ -10,6 +10,7 @@ import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
+import android.view.WindowInsets;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
@@ -127,6 +128,7 @@ public class LogActivity extends Activity {
});
setContentView(root);
+ applyInsets(root, dp(16), dp(14), dp(16), dp(12));
}
private void appendLogLine(String line) {
@@ -177,4 +179,27 @@ public class LogActivity extends Activity {
private int dp(int value) {
return Math.round(value * getResources().getDisplayMetrics().density);
}
+
+ /**
+ * Pads the root by the system bar insets. Required from API 35, where the
+ * activity is laid out edge to edge and would otherwise draw under the
+ * status and navigation bars.
+ */
+ private void applyInsets(final View root, final int l, final int t, final int r, final int b) {
+ root.setOnApplyWindowInsetsListener((v, insets) -> {
+ int top, bottom, left, right;
+ if (Build.VERSION.SDK_INT >= 30) {
+ android.graphics.Insets bars = insets.getInsets(WindowInsets.Type.systemBars());
+ top = bars.top; bottom = bars.bottom; left = bars.left; right = bars.right;
+ } else {
+ top = insets.getSystemWindowInsetTop();
+ bottom = insets.getSystemWindowInsetBottom();
+ left = insets.getSystemWindowInsetLeft();
+ right = insets.getSystemWindowInsetRight();
+ }
+ v.setPadding(l + left, t + top, r + right, b + bottom);
+ return insets;
+ });
+ root.requestApplyInsets();
+ }
}
diff --git a/android/src/com/dragontcp/client/MainActivity.java b/android/src/com/dragontcp/client/MainActivity.java
index 561a4d5..96f6b69 100644
--- a/android/src/com/dragontcp/client/MainActivity.java
+++ b/android/src/com/dragontcp/client/MainActivity.java
@@ -19,6 +19,7 @@ import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
+import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
@@ -28,6 +29,7 @@ import android.widget.Toast;
public class MainActivity extends Activity {
private static final int VPN_REQUEST = 100;
+ private static final int NOTIF_REQUEST = 101;
private static final String PREFS = "dragontcp";
private static final int BATCH_LIMIT = 256;
@@ -86,6 +88,7 @@ public class MainActivity extends Activity {
forceDarkSystemUi();
buildUi();
loadSettings();
+ ensureNotificationPermission();
updateBatchHint();
}
@@ -93,7 +96,15 @@ public class MainActivity extends Activity {
protected void onStart() {
super.onStart();
if (!receiverRegistered) {
- registerReceiver(stateReceiver, new IntentFilter(DragonService.ACTION_STATE));
+ // From API 33 the export flag is mandatory. This receiver only ever
+ // hears DragonService's own package-scoped broadcast, so it must be
+ // NOT_EXPORTED; omitting the flag throws SecurityException.
+ IntentFilter filter = new IntentFilter(DragonService.ACTION_STATE);
+ if (Build.VERSION.SDK_INT >= 33) {
+ registerReceiver(stateReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
+ } else {
+ registerReceiver(stateReceiver, filter);
+ }
receiverRegistered = true;
}
updateConnectionUi(DragonService.isActive(), DragonService.currentStatus());
@@ -110,15 +121,59 @@ public class MainActivity extends Activity {
private void forceDarkSystemUi() {
Window w = getWindow();
- w.setStatusBarColor(BG);
- w.setNavigationBarColor(Color.rgb(7, 10, 14));
+ // setStatusBarColor / setNavigationBarColor are no-ops once the app
+ // targets API 35+, where every activity is laid out edge to edge. The
+ // window background supplies the colour instead and applyInsets() keeps
+ // content clear of the system bars.
+ if (Build.VERSION.SDK_INT < 35) {
+ w.setStatusBarColor(BG);
+ w.setNavigationBarColor(Color.rgb(7, 10, 14));
+ }
if (Build.VERSION.SDK_INT >= 26) w.getDecorView().setSystemUiVisibility(0);
}
+ /**
+ * Pads the root view by the system bar insets. Required from API 35: the
+ * activity draws edge to edge, so without this the header sits under the
+ * status bar and the buttons under the navigation bar.
+ */
+ private void applyInsets(final View root, final int l, final int t, final int r, final int b) {
+ root.setOnApplyWindowInsetsListener((v, insets) -> {
+ int top, bottom, left, right;
+ if (Build.VERSION.SDK_INT >= 30) {
+ android.graphics.Insets bars =
+ insets.getInsets(WindowInsets.Type.systemBars() | WindowInsets.Type.ime());
+ top = bars.top; bottom = bars.bottom; left = bars.left; right = bars.right;
+ } else {
+ top = insets.getSystemWindowInsetTop();
+ bottom = insets.getSystemWindowInsetBottom();
+ left = insets.getSystemWindowInsetLeft();
+ right = insets.getSystemWindowInsetRight();
+ }
+ v.setPadding(l + left, t + top, r + right, b + bottom);
+ return insets;
+ });
+ root.requestApplyInsets();
+ }
+
+ /** From API 33 the ongoing VPN notification is hidden without this grant. */
+ private void ensureNotificationPermission() {
+ if (Build.VERSION.SDK_INT < 33) return;
+ if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS)
+ == android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ return;
+ }
+ try {
+ requestPermissions(new String[]{ android.Manifest.permission.POST_NOTIFICATIONS }, NOTIF_REQUEST);
+ } catch (Throwable ignored) {
+ }
+ }
+
private void buildUi() {
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(BG);
+ // Base padding; applyInsets() adds the system bar insets on top.
root.setPadding(dp(16), dp(14), dp(16), dp(12));
LinearLayout header = new LinearLayout(this);
@@ -274,6 +329,7 @@ public class MainActivity extends Activity {
logsButton.setOnClickListener(v -> startActivity(new Intent(this, LogActivity.class)));
setContentView(root);
+ applyInsets(root, dp(16), dp(14), dp(16), dp(12));
updateConnectionUi(false, "DISCONNECTED");
}
diff --git a/build_core.ps1 b/build_core.ps1
index b1cec36..cbb6496 100644
--- a/build_core.ps1
+++ b/build_core.ps1
@@ -3,29 +3,37 @@
Windows equivalent of build_core.sh - builds the Go core binaries.
.DESCRIPTION
- Cross-compiles the Android ARM64 client (dropped into android\lib\arm64-v8a\
- as libdragontcp_client.so) and the Linux AMD64/ARM64 servers.
- CGO is disabled, so no C toolchain or NDK is required.
+ Builds the Android client for every shipped ABI and the Linux client/servers.
+
+ arm64-v8a builds with CGO disabled and needs no NDK. The other Android ABIs
+ do: `GOOS=android` requires external linking for arm, amd64 and 386, so an
+ NDK toolchain is located automatically. If none is found those ABIs are
+ skipped with a warning rather than failing the build - the APK is still
+ installable on 64-bit ARM, which is the overwhelming majority of devices.
.EXAMPLE
.\build_core.ps1
.\build_core.ps1 -ClientOnly
+ .\build_core.ps1 -Abis arm64-v8a,armeabi-v7a
#>
[CmdletBinding()]
param(
- # Only build the Android client .so (skip the Linux servers).
+ # Only build the Android client (skip the Linux client and servers).
[switch]$ClientOnly,
- # Where to drop libdragontcp_client.so. Defaults to android\lib\arm64-v8a
- # under this script; build_apk.ps1 passes its own app directory explicitly,
- # which is what makes this work regardless of how the tree is nested.
- [string]$AndroidLibDir,
- # Path to the go executable.
+ # Root of the APK's lib directory; per-ABI subdirectories are created here.
+ [string]$AndroidLibRoot,
+ # Which Android ABIs to build.
+ [string[]]$Abis = @('arm64-v8a', 'armeabi-v7a', 'x86_64'),
+ # NDK root; auto-detected from the SDK when omitted.
+ [string]$NdkRoot,
+ # Minimum Android API the native cores are built for.
+ [int]$MinApi = 24,
[string]$GoBin = $(if ($env:GO_BIN) { $env:GO_BIN } else { 'go' })
)
$ErrorActionPreference = 'Stop'
$Root = $PSScriptRoot
-if (-not $AndroidLibDir) { $AndroidLibDir = Join-Path $Root 'android\lib\arm64-v8a' }
+if (-not $AndroidLibRoot) { $AndroidLibRoot = Join-Path $Root 'android\lib' }
function Invoke-Go {
param([hashtable]$Env, [string[]]$GoArgs)
@@ -42,17 +50,89 @@ function Invoke-Go {
}
}
+# Locate an NDK llvm toolchain bin directory, newest first.
+function Find-NdkBin {
+ param([string]$Explicit)
+ $roots = @()
+ if ($Explicit) { $roots += $Explicit }
+ if ($env:ANDROID_NDK_ROOT) { $roots += $env:ANDROID_NDK_ROOT }
+ if ($env:ANDROID_NDK_HOME) { $roots += $env:ANDROID_NDK_HOME }
+ foreach ($sdk in $env:ANDROID_SDK_ROOT, $env:ANDROID_HOME, "$env:LOCALAPPDATA\Android\Sdk") {
+ if ($sdk -and (Test-Path (Join-Path $sdk 'ndk'))) {
+ $roots += (Get-ChildItem (Join-Path $sdk 'ndk') -Directory |
+ Sort-Object { try { [version]$_.Name } catch { [version]'0.0.0' } } -Descending |
+ ForEach-Object { $_.FullName })
+ }
+ }
+ foreach ($r in $roots) {
+ if (-not $r) { continue }
+ $bin = Join-Path $r 'toolchains\llvm\prebuilt\windows-x86_64\bin'
+ if (Test-Path $bin) { return $bin }
+ }
+ return $null
+}
+
+# ABI -> (GOARCH, GOARM, clang triple prefix)
+$abiSpec = @{
+ 'arm64-v8a' = @{ arch = 'arm64'; arm = ''; cc = 'aarch64-linux-android' }
+ 'armeabi-v7a' = @{ arch = 'arm'; arm = '7'; cc = 'armv7a-linux-androideabi' }
+ 'x86_64' = @{ arch = 'amd64'; arm = ''; cc = 'x86_64-linux-android' }
+ 'x86' = @{ arch = '386'; arm = ''; cc = 'i686-linux-android' }
+}
+
$go = Get-Command $GoBin -ErrorAction SilentlyContinue
if (-not $go) { throw "Go compiler not found. Install from https://go.dev/dl/ or set -GoBin." }
-New-Item -ItemType Directory -Force -Path "$Root\bin", $AndroidLibDir | Out-Null
+$ndkBin = Find-NdkBin $NdkRoot
+New-Item -ItemType Directory -Force -Path "$Root\bin" | Out-Null
Push-Location "$Root\core"
try {
$ldflags = '-ldflags=-s -w'
+ $built = @()
- Write-Host '[core] Android ARM64 client...' -ForegroundColor Cyan
- Invoke-Go @{ CGO_ENABLED = '0'; GOOS = 'android'; GOARCH = 'arm64' } `
- @('build', '-trimpath', $ldflags, '-o', (Join-Path $AndroidLibDir 'libdragontcp_client.so'), './cmd/dragontcp-client')
+ foreach ($abi in $Abis) {
+ if (-not $abiSpec.ContainsKey($abi)) { Write-Warning "unknown ABI $abi, skipping"; continue }
+ $spec = $abiSpec[$abi]
+ $outDir = Join-Path $AndroidLibRoot $abi
+ $out = Join-Path $outDir 'libdragontcp_client.so'
+
+ # arm64 links internally; everything else needs the NDK's clang.
+ $env = @{ GOOS = 'android'; GOARCH = $spec.arch }
+ if ($spec.arm) { $env['GOARM'] = $spec.arm }
+ if ($abi -eq 'arm64-v8a') {
+ $env['CGO_ENABLED'] = '0'
+ } else {
+ if (-not $ndkBin) {
+ Write-Warning "[core] $abi skipped: no NDK found (set -NdkRoot or ANDROID_NDK_ROOT)"
+ continue
+ }
+ $cc = Join-Path $ndkBin ("{0}{1}-clang.cmd" -f $spec.cc, $MinApi)
+ if (-not (Test-Path $cc)) {
+ Write-Warning "[core] $abi skipped: $cc not found"
+ continue
+ }
+ $env['CGO_ENABLED'] = '1'
+ $env['CC'] = $cc
+ }
+
+ Write-Host "[core] Android $abi client..." -ForegroundColor Cyan
+ New-Item -ItemType Directory -Force -Path $outDir | Out-Null
+ Invoke-Go $env @('build', '-trimpath', $ldflags, '-o', $out, './cmd/dragontcp-client')
+ $built += $abi
+ }
+
+ if (-not $built) { throw 'No Android ABI was built.' }
+ Write-Host "[core] Android ABIs: $($built -join ', ')" -ForegroundColor Green
+
+ # Remove stale ABI directories so the APK never ships an out-of-date core.
+ if (Test-Path $AndroidLibRoot) {
+ foreach ($dir in Get-ChildItem $AndroidLibRoot -Directory) {
+ if ($built -notcontains $dir.Name) {
+ Write-Warning "[core] removing stale $($dir.Name) native library"
+ Remove-Item $dir.FullName -Recurse -Force
+ }
+ }
+ }
if (-not $ClientOnly) {
Write-Host '[core] Linux AMD64 client...' -ForegroundColor Cyan