Files
strongswan-ext/src/frontends/android/app/build.gradle
T
DenozordecandClaude Fable 5.2 fdef56fa29
Android / pre-check (push) Failing after 4s
Android / android (push) Skipped
CIFuzz / pre-check (push) Failing after 5s
CIFuzz / fuzzing (address) (push) Skipped
CIFuzz / fuzzing (undefined) (push) Skipped
CodeQL / pre-check (push) Failing after 5s
CodeQL / analyze (python) (push) Skipped
CodeQL / analyze (ruby) (push) Skipped
CodeQL / analyze-cpp (push) Skipped
FreeBSD / pre-check (push) Failing after 4s
FreeBSD / freebsd (14.4) (push) Skipped
FreeBSD / freebsd (15.0) (push) Skipped
Linux / pre-check (push) Failing after 5s
Linux / latest (apidoc) (push) Skipped
Linux / latest (clang, no, all) (push) Skipped
Linux / latest (clang, no, default) (push) Skipped
Linux / latest (clang, no, printf-builtin) (push) Skipped
Linux / latest (clang, no-dbg) (push) Skipped
Linux / latest (clang, no-testable-ke) (push) Skipped
Linux / latest (clang, yes, all) (push) Skipped
Linux / latest (clang, yes, default) (push) Skipped
Linux / latest (clang, yes, fuzzing) (push) Skipped
Linux / latest (coverage) (push) Skipped
Linux / latest (dist) (push) Skipped
Linux / latest (gcc, no, all) (push) Skipped
Linux / latest (gcc, no, default) (push) Skipped
Linux / latest (gcc, no, printf-builtin) (push) Skipped
Linux / latest (gcc, yes, all) (push) Skipped
Linux / latest (gcc, yes, default) (push) Skipped
Linux / latest (nm) (push) Skipped
Linux / latest (no-dbg) (push) Skipped
Linux / latest (no-testable-ke) (push) Skipped
Linux / latest (yes, ld) (push) Skipped
Linux / crypto (ubuntu-22.04, openssl-sys) (push) Skipped
Linux / crypto (ubuntu-latest, botan) (push) Skipped
Linux / crypto (ubuntu-latest, openssl-3) (push) Skipped
Linux / crypto (ubuntu-latest, openssl-4) (push) Skipped
Linux / crypto (ubuntu-latest, openssl-awslc) (push) Skipped
Linux / crypto (ubuntu-latest, openssl-sys) (push) Skipped
Linux / crypto (ubuntu-latest, wolfssl) (push) Skipped
Linux / older (clang, ubuntu-22.04, all) (push) Skipped
Linux / older (gcc, ubuntu-22.04, all) (push) Skipped
Linux / older (gcc, ubuntu-22.04, nm) (push) Skipped
Linux / alpine (push) Skipped
macOS / pre-check (push) Failing after 5s
macOS / macos (macos-14) (push) Skipped
macOS / macos (macos-latest) (push) Skipped
SonarCloud / pre-check (push) Failing after 5s
SonarCloud / sonarcloud (push) Skipped
TKM / pre-check (push) Failing after 5s
TKM / tkm (push) Skipped
Windows / pre-check (push) Failing after 6s
Windows / cross-compile (win32) (push) Skipped
Windows / cross-compile (win64) (push) Skipped
Windows / native (i686, mingw32, win32) (push) Skipped
Windows / native (x86_64, mingw64, win64) (push) Skipped
android: version 2.19.13 (181)
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-13 22:41:14 +07:00

236 lines
11 KiB
Groovy

apply plugin: 'com.android.application'
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
/* Optional .env next to this project: S3/update settings and signing (see .env.example).
Environment variables take precedence over .env values. */
def dotenv = [:]
def envFile = rootProject.file('.env')
if (envFile.exists()) {
envFile.eachLine { line ->
def trimmed = line.trim()
if (trimmed && !trimmed.startsWith('#') && trimmed.contains('=')) {
def idx = trimmed.indexOf('=')
dotenv[trimmed.substring(0, idx).trim()] = trimmed.substring(idx + 1).trim()
}
}
}
def envOr = { String key, String defaultValue ->
(System.getenv(key) ?: dotenv[key] ?: defaultValue)
}
def updateBaseUrl = envOr('S3_PUBLIC_BASE_URL', '').replace('\\', '\\\\').replace('"', '\\"')
def keystoreFile = envOr('KEYSTORE_FILE', '')
def keystorePassword = envOr('KEYSTORE_PASSWORD', '')
/* note: locals must not be named keyAlias/keyPassword - those names would
shadow the signingConfig methods inside the signingConfigs block */
def keystoreKeyAlias = envOr('KEY_ALIAS', '')
def keystoreKeyPassword = envOr('KEY_PASSWORD', '')
android {
namespace = 'org.strongswan.android'
/* minSdk >= 23 flips AGP's default to uncompressed .so (extractNativeLibs=false):
* the APK grew 12 -> 23 MB. Updates are downloaded over cellular, so keep
* the libraries compressed as before. */
packaging {
jniLibs {
useLegacyPackaging = true
}
}
defaultConfig {
applicationId "one.shx.strongswan.ext"
compileSdk = 37
minSdk = 24
targetSdk = 36
versionCode 181
versionName "2.19.13"
/* Base URL of the update storage, trailing slash included */
buildConfigField "String", "UPDATE_BASE_URL", "\"${updateBaseUrl}\""
/* S3 credentials used by the app for authenticated update downloads
(same keys as the publish script). They are obfuscated with XOR +
Base64 at build time to hinder trivial string extraction.
Note: obfuscation is not real protection - an APK can always be
reverse engineered, so rotate these keys if they leak. */
def updateS3Access = envOr('S3_ACCESS_KEY', '')
def updateS3Secret = envOr('S3_SECRET_KEY', '')
def updateXorKey = new byte[16]
System.arraycopy(java.security.MessageDigest.getInstance('SHA-256')
.digest('shx-update-credentials'.getBytes('UTF-8')), 0, updateXorKey, 0, 16)
def xorB64 = { String value ->
if (!value) { return '' }
def bytes = value.getBytes('UTF-8')
for (int i = 0; i < bytes.length; i++) {
bytes[i] = (byte)(bytes[i] ^ updateXorKey[i % updateXorKey.length])
}
java.util.Base64.getEncoder().encodeToString(bytes)
}
buildConfigField "String", "UPDATE_S3_ACCESS_KEY", "\"${xorB64(updateS3Access)}\""
buildConfigField "String", "UPDATE_S3_SECRET_KEY", "\"${xorB64(updateS3Secret)}\""
buildConfigField "String", "UPDATE_S3_KEY_XOR", "\"${java.util.Base64.getEncoder().encodeToString(updateXorKey)}\""
buildConfigField "String", "UPDATE_S3_REGION", "\"${envOr('S3_REGION', 'us-east-1')}\""
/* SHX update API (web panel). When UPDATE_API_URL, UPDATE_API_PUBLIC_KEY and UPDATE_API_HMAC_KEY are all set,
the app checks for updates there; the bucket path above stays as a fallback while the API is unreachable.
UPDATE_API_PUBLIC_KEY is the raw Ed25519 public key (base64, 32 bytes) shown on the panel's "API updates" page;
the HMAC key is only a guard against casual requests (an APK can always be unpacked), integrity rests on the
manifest signature and on the SHA-256 plus APK signature checks. */
buildConfigField "String", "UPDATE_API_URL", "\"${envOr('UPDATE_API_URL', '')}\""
buildConfigField "String", "UPDATE_API_PUBLIC_KEY", "\"${envOr('UPDATE_API_PUBLIC_KEY', '')}\""
buildConfigField "String", "UPDATE_API_HMAC_KEY", "\"${xorB64(envOr('UPDATE_API_HMAC_KEY', ''))}\""
/* public download page, opened by «Download from the site» when an update fails */
buildConfigField "String", "UPDATE_DOWNLOAD_PAGE_URL", "\"${envOr('UPDATE_DOWNLOAD_PAGE_URL', 'https://get.shnt.top/get/universal')}\""
externalNativeBuild {
ndkBuild {
arguments '-j' + Runtime.runtime.availableProcessors()
/* deep checkout paths (e.g. git worktrees) overflow the Windows command line; opt in with -PshortNdkCommands */
if (project.hasProperty('shortNdkCommands')) {
arguments 'APP_SHORT_COMMANDS=true', 'LOCAL_SHORT_COMMANDS=true'
}
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
}
}
}
ndkVersion = "27.3.13750724"
externalNativeBuild {
ndkBuild {
path 'src/main/jni/Android.mk'
}
}
tasks.withType(JavaCompile).configureEach {
compileTask ->
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
}
signingConfigs {
release {
if (keystoreFile) {
storeFile rootProject.file(keystoreFile)
storePassword keystorePassword
keyAlias keystoreKeyAlias
keyPassword keystoreKeyPassword
}
}
}
buildTypes {
release {
/* R8 shrinking + resource shrinking: strips dead code/resources and
obfuscates. The JNI surface is kept via proguard-rules.txt.
Smoke-test a release build on a device before publishing. */
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt'
if (keystoreFile) {
signingConfig signingConfigs.release
} else {
/* fall back to the debug key so that release APKs update debug-signed
installations in place (no uninstall, data is preserved) */
def debugKeystore = new File(System.getProperty('user.home'), '.android/debug.keystore')
if (debugKeystore.exists()) {
signingConfig signingConfigs.debug
println "WARNING: KEYSTORE_FILE not set (see .env.example) - signing the release APK with the debug key (${debugKeystore})"
} else {
println "WARNING: no signing configuration - the release APK will be UNSIGNED (set KEYSTORE_FILE in .env or run a debug build once)"
}
}
}
}
compileOptions {
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_11
}
buildFeatures {
compose true
buildConfig true
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.core:core:1.17.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'androidx.lifecycle:lifecycle-process:2.9.4'
implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.9.4'
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.9.4'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.work:work-runtime-ktx:2.10.1'
implementation 'com.google.android.material:material:1.13.0'
/* BOM 2026.09.00 = compose 1.12. The Expressive API (MaterialExpressiveTheme,
MotionScheme.expressive, LoadingIndicator, MaterialShapes) lives only in
material3 1.5.0-alpha/beta, so material3 is pinned explicitly (needs AGP 9.1,
compileSdk 37, minSdk 24). Unpin once 1.5.0 is stable. */
def composeBom = platform('androidx.compose:compose-bom:2026.09.00')
implementation composeBom
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.material3:material3:1.5.0-beta01'
/* adaptive-navigation-suite (NavigationSuiteScaffold) requires minSdk 23+ (material3 1.5 requires 24) */
implementation 'androidx.compose.material3:material3-adaptive-navigation-suite:1.5.0-beta01'
/* list-detail layout for Profiles on expanded widths */
implementation 'androidx.compose.material3.adaptive:adaptive-layout:1.3.0'
implementation 'androidx.compose.material3.adaptive:adaptive-navigation:1.3.0'
implementation 'androidx.compose.material:material-icons-extended'
/* home screen widget (status + connect button) */
implementation 'androidx.glance:glance-appwidget:1.2.0'
implementation 'androidx.glance:glance-material3:1.2.0'
implementation 'androidx.activity:activity-compose:1.11.0'
/* QR: scanning profiles (camera) and showing a profile as a QR code */
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
/* Ed25519 verification of the signed update manifest (java.security Ed25519 needs API 33, minSdk is 24) */
implementation 'net.i2p.crypto:eddsa:0.3.0'
debugImplementation 'androidx.compose.ui:ui-tooling'
implementation 'androidx.compose.ui:ui-tooling-preview'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.assertj:assertj-core:3.27.6'
testImplementation 'org.mockito:mockito-core:5.20.0'
/* the android.jar org.json stubs throw on the JVM; the real implementation is needed for the update API tests */
testImplementation 'org.json:json:20240303'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestImplementation 'org.assertj:assertj-core:3.27.6'
androidTestImplementation composeBom
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
}
/* Publish to the SHX VPN panel (https://apk.shnt.top): build the release APK and upload it in one step.
./gradlew uploadShx -> draft in the beta channel, type phone
./gradlew uploadShx -PshxPublish -PshxRollout=25
./gradlew uploadShx -PshxChannel=release -PshxType=tv -PshxNotes="..."
Needs Node 18+ and SHX_URL, SHX_TOKEN (a CI token from the panel's Settings -> Tokens).
Release notes default to NOTES_RU from .env; the version comes from the build's output-metadata.json. */
tasks.register('uploadShx', Exec) {
group = 'publishing'
description = 'Build the release APK and upload it to the SHX VPN panel'
dependsOn 'assembleRelease'
def apkFile = layout.buildDirectory.file('outputs/apk/release/app-release.apk')
def script = rootProject.file('scripts/upload-apk.mjs')
doFirst {
def apk = apkFile.get().asFile
if (!apk.exists()) {
throw new GradleException("${apk} not found: the release APK is unsigned (set KEYSTORE_FILE in .env)")
}
def args = ['node', script.absolutePath, apk.absolutePath,
'--type', (findProperty('shxType') ?: envOr('SHX_TYPE', 'phone')).toString(),
'--channel', (findProperty('shxChannel') ?: 'beta').toString()]
def notes = (findProperty('shxNotes') ?: envOr('NOTES_RU', '')).toString()
if (notes) args += ['--notes', notes]
if (project.hasProperty('shxPublish')) {
args += ['--publish', '--rollout', (findProperty('shxRollout') ?: '100').toString()]
}
commandLine args
}
}