Skip to content
Closed
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
4 changes: 4 additions & 0 deletions .github/workflows/build-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ on:
installer_base_name:
required: true
type: string
android_identity_seed:
required: false
type: string

jobs:
build-android:
Expand Down Expand Up @@ -154,6 +157,7 @@ jobs:
BUILD_TYPE: ${{ inputs.build_type }}
VERSION: ${{ inputs.version }}
INSTALLER_NAME: ${{ inputs.installer_base_name }}
ANDROID_IDENTITY_SEED: ${{ inputs.android_identity_seed }}
GOMOBILECACHE: ${{ env.GOMOBILECACHE }}

- name: Upload Android APK
Expand Down
210 changes: 164 additions & 46 deletions Makefile

Large diffs are not rendered by default.

208 changes: 204 additions & 4 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,169 @@ def start = new Date(2015, 1, 1).getTime()
def now = System.currentTimeMillis()
def code = (int)((now - start) / 1000)

def defaultStealthProfile = [
mode : "normal",
packageName : "org.getlantern.lantern",
appName : "Lantern",
sessionName : "LanternVpn",
denylistVersion : 0,
]

def stealthModeAliases = [
vpn : "stealth-vpn",
novpn: "stealth-novpn",
]

def normalizeStealthMode = { value ->
def mode = value.toString().trim()
return stealthModeAliases.get(mode, mode)
}

def buildConfigString = { value ->
def escaped = value.toString()
.replace("\\", "\\\\")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
.replace("\b", "\\b")
.replace("\f", "\\f")
.replace("\"", "\\\"")
return "\"${escaped}\""
}

def manifestPlaceholderString = { fieldName, value ->
def text = value.toString()
if (!text.trim()) {
throw new GradleException("Stealth profile ${fieldName} must not be empty")
}
for (int i = 0; i < text.length(); i++) {
def ch = text.charAt(i)
if (Character.isISOControl(ch) || ['"', "'", '&', '<', '>'].contains(String.valueOf(ch))) {
throw new GradleException(
"Stealth profile ${fieldName} contains XML-reserved or control characters"
)
}
}
return text
}

def nonNegativeInteger = { fieldName, value ->
try {
def parsed = Integer.parseInt(value.toString())
if (parsed < 0) {
throw new NumberFormatException("negative")
}
return parsed
} catch (NumberFormatException ignored) {
throw new GradleException("Stealth profile ${fieldName} must be a non-negative integer")
}
}

def androidApplicationId = { fieldName, value ->
if (!(value instanceof CharSequence)) {
throw new GradleException("Stealth profile ${fieldName} must be a string")
}
def text = value.toString().trim()
if (!(text ==~ /[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+/)) {
throw new GradleException(
"Stealth profile ${fieldName} must be a valid Android applicationId"
)
}
return text
}

def resolveRepoRelativeFile = { path ->
def candidate = new File(path.toString())
if (candidate.isAbsolute()) {
return candidate
}
def fromRepoRoot = new File(rootProject.projectDir.parentFile, path.toString())
if (fromRepoRoot.exists()) {
return fromRepoRoot
}
return file(path.toString())
}

def loadStealthProfile = {
def profilePath = (findProperty("stealthProfile") ?: System.getenv("STEALTH_PROFILE"))?.toString()?.trim()
if (!profilePath) {
return defaultStealthProfile
}

def profileFile = resolveRepoRelativeFile(profilePath)
if (!profileFile.exists()) {
throw new GradleException("Stealth profile not found: ${profilePath}")
}

def parsed = new groovy.json.JsonSlurper().parse(profileFile)
if (!(parsed instanceof Map)) {
throw new GradleException("Stealth profile must be a JSON object: ${profileFile}")
}

def profile = defaultStealthProfile + parsed.findAll { it.value != null }
profile.mode = normalizeStealthMode(profile.mode)
if (!["stealth-vpn", "stealth-novpn"].contains(profile.mode)) {
throw new GradleException(
"Unsupported Stealth mode '${profile.mode}' in ${profileFile}; expected stealth-vpn, stealth-novpn, vpn, or novpn"
)
}
profile.appName = manifestPlaceholderString("appName", profile.appName)
profile.sessionName = manifestPlaceholderString("sessionName", profile.sessionName)
profile.denylistVersion = nonNegativeInteger("denylistVersion", profile.denylistVersion)
profile.packageName = androidApplicationId("packageName", profile.packageName)
return profile
}

def stealthProfile = loadStealthProfile()

// Android identity — UI strings for notification channels, quick tile, etc.
// applicationId always comes from stealthProfile.packageName (single source of truth).
def androidIdentityDefaults = [
appLabel: "Lantern",
launcherLabel: "Lantern",
identityLabel: "Lantern",
identityProfileId: "standard",
identityMetadata: "{}",
vpnSessionName: "LanternVpn",
notificationChannelVpn: "VPN",
notificationChannelDataUsage: "Data Usage",
notificationTitle: "Lantern",
notificationConnectedText: "Lantern VPN is running",
notificationStartingText: "Starting Lantern VPN...",
notificationDisconnectAction: "Disconnect",
quickTileActiveLabel: "VPN Connected",
quickTileInactiveLabel: "VPN Disconnected",
appIcon: "@mipmap/ic_launcher",
appRoundIcon: "@mipmap/ic_launcher_round",
notificationSmallIcon: "@drawable/lantern_notification_icon",
quickTileIcon: "@drawable/lantern_notification_icon",
appAuthScheme: "lantern",
]

def androidIdentityProfilePath =
(project.findProperty("androidIdentityProfile") ?: System.getenv("ANDROID_IDENTITY_PROFILE"))?.toString()?.trim()
def androidIdentity = new LinkedHashMap(androidIdentityDefaults)
if (androidIdentityProfilePath) {
def profileFile = new File(androidIdentityProfilePath)
if (!profileFile.isAbsolute()) {
profileFile = new File(rootProject.projectDir.parentFile, androidIdentityProfilePath)
}
if (!profileFile.exists()) {
throw new GradleException("Android identity profile not found: ${profileFile}")
}
def props = new java.util.Properties()
profileFile.withReader("UTF-8") { props.load(it) }
props.each { key, value ->
def name = key.toString()
if (name == "applicationId") {
return // applicationId comes from stealthProfile.packageName only
}
if (!androidIdentity.containsKey(name)) {
throw new GradleException("Unknown Android identity profile key '${name}' in ${profileFile}")
}
androidIdentity[name] = value.toString()
}
}
android {
namespace = "org.getlantern.lantern"
compileSdk = 36
Expand Down Expand Up @@ -76,13 +239,21 @@ android {
jvmTarget = "17"
}

buildFeatures {
buildConfig true
}

// arm64-only for every artifact (APK + AAB). armeabi-v7a (32-bit) was
// dropped: Go >=1.23.2 trips Android 8-10 seccomp on 32-bit, killing
// libgojni.so with SIGSYS at startup (golang/go#70495 — ~54% of v9
// crashes). Constraint: abiFilters must match the ABIs Flutter built
// (Makefile --target-platform), or installs crash with "Could not find
// 'libflutter.so'".

buildFeatures {
buildConfig true
}

// Use legacy packaging to help reduce apk size
packagingOptions {
exclude "DebugProbesKt.bin"
Expand All @@ -109,11 +280,30 @@ android {
}

defaultConfig {
applicationId = "org.getlantern.lantern"
applicationId = stealthProfile.packageName.toString()
minSdkVersion = 24
targetSdk = 36
versionCode = code
versionName = flutter.versionName
manifestPlaceholders = [
appName: stealthProfile.appName.toString(),
appLabel: androidIdentity.appLabel,
launcherLabel: androidIdentity.launcherLabel,
appIcon: androidIdentity.appIcon,
appRoundIcon: androidIdentity.appRoundIcon,
identityLabel: androidIdentity.identityLabel,
identityProfileId: androidIdentity.identityProfileId,
identityMetadata: androidIdentity.identityMetadata,
quickTileIcon: androidIdentity.quickTileIcon,
appAuthScheme: androidIdentity.appAuthScheme,
Comment thread
reflog marked this conversation as resolved.
]
resValue "string", "app_name", androidIdentity.appLabel
buildConfigField "boolean", "STEALTH_ENABLED", (stealthProfile.mode.toString() != "normal").toString()
buildConfigField "String", "STEALTH_MODE", buildConfigString(stealthProfile.mode)
buildConfigField "String", "STEALTH_PACKAGE_NAME", buildConfigString(stealthProfile.packageName)
buildConfigField "String", "STEALTH_APP_NAME", buildConfigString(stealthProfile.appName)
buildConfigField "String", "STEALTH_SESSION_NAME", buildConfigString(stealthProfile.sessionName)
buildConfigField "int", "STEALTH_DENYLIST_VERSION", stealthProfile.denylistVersion.toString()
buildConfigField "String", "SIDELOAD_SIGNING_CERTIFICATE_SHA256", "\"${sideloadSigningCertificateSha256}\""

ndk {
Expand All @@ -129,9 +319,19 @@ android {
arguments "-DANDROID_ARM_NEON=TRUE", '-DANDROID_STL=c++_shared'
}
}
buildFeatures {
buildConfig true
}
buildConfigField "String", "ANDROID_IDENTITY_LABEL", buildConfigString(androidIdentity.identityLabel)
buildConfigField "String", "ANDROID_IDENTITY_PROFILE_ID", buildConfigString(androidIdentity.identityProfileId)
buildConfigField "String", "ANDROID_IDENTITY_METADATA", buildConfigString(androidIdentity.identityMetadata)
buildConfigField "String", "VPN_SESSION_NAME", buildConfigString(androidIdentity.vpnSessionName)
buildConfigField "String", "NOTIFICATION_CHANNEL_VPN", buildConfigString(androidIdentity.notificationChannelVpn)
buildConfigField "String", "NOTIFICATION_CHANNEL_DATA_USAGE", buildConfigString(androidIdentity.notificationChannelDataUsage)
buildConfigField "String", "NOTIFICATION_TITLE", buildConfigString(androidIdentity.notificationTitle)
buildConfigField "String", "NOTIFICATION_CONNECTED_TEXT", buildConfigString(androidIdentity.notificationConnectedText)
buildConfigField "String", "NOTIFICATION_STARTING_TEXT", buildConfigString(androidIdentity.notificationStartingText)
buildConfigField "String", "NOTIFICATION_DISCONNECT_ACTION", buildConfigString(androidIdentity.notificationDisconnectAction)
buildConfigField "String", "QUICK_TILE_ACTIVE_LABEL", buildConfigString(androidIdentity.quickTileActiveLabel)
buildConfigField "String", "QUICK_TILE_INACTIVE_LABEL", buildConfigString(androidIdentity.quickTileInactiveLabel)
buildConfigField "String", "NOTIFICATION_SMALL_ICON", buildConfigString(androidIdentity.notificationSmallIcon)
}

signingConfigs {
Expand Down
16 changes: 8 additions & 8 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@

<application
android:name=".LanternApp"
android:icon="@mipmap/ic_launcher"
android:label="Lantern"
android:icon="${appIcon}"
android:label="${appName}"
android:usesCleartextTraffic="true"
android:roundIcon="@mipmap/ic_launcher_round">
android:roundIcon="${appRoundIcon}">

<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:label="${launcherLabel}"
android:launchMode="singleTask"
android:taskAffinity=""
android:screenOrientation="portrait"
Expand Down Expand Up @@ -65,19 +66,19 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lantern" android:host="auth" />
<data android:scheme="${appAuthScheme}" android:host="auth" />
</intent-filter>
Comment thread
reflog marked this conversation as resolved.
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lantern" android:host="report-issue" />
<data android:scheme="${appAuthScheme}" android:host="report-issue" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lantern" android:host="private-server" />
<data android:scheme="${appAuthScheme}" android:host="private-server" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
Expand Down Expand Up @@ -122,7 +123,6 @@
android:name="flutterEmbedding"
android:value="2" />


<service
android:name=".service.LanternVpnService"
android:exported="false"
Expand All @@ -137,7 +137,7 @@
android:name=".service.QuickTileService"
android:directBootAware="true"
android:exported="true"
android:icon="@drawable/lantern_notification_icon"
android:icon="${quickTileIcon}"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="n">
<intent-filter>
Expand Down
Loading
Loading