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