Commit 92f17d94b7 for strongswan.org
commit 92f17d94b77626c2c4e4fe2055a431096c3f45c8
Author: Tobias Brunner <tobias@strongswan.org>
Date: Thu Oct 22 15:43:13 2026 +0200
android: Enable shrinking/optimization/obfuscation in release builds
Google requires that apps enable this to avoid getting demoted in the
store or even get denied updates (at least 25% reduction in each category
is required before Feb 2027). Due to JNI we have to mark several classes
and methods that are called from C code, which the tools can't detect
automatically, from getting removed/optimized. Luckily, the rules
merged in via androidx handle the androidx.annotation.Keep attribute
appropriately so we don't have to write any custom rules (the
proguard-rules.txt file is still added for experiments and e.g. analysis
via `-printusage`).
Besides classes needed for JNI, the classes related to the local
certificate store also have to be marked because those are instantiated
via reflection.
According to the R8 configuration analyzer this gives us a reduction of
at least 94% in each category.
Also added new build type that allows testing the app with optimization
and obfuscation locally (disabled for debuggable builds).
diff --git a/src/frontends/android/app/build.gradle b/src/frontends/android/app/build.gradle
index fbdb64905c..8c60976cf6 100644
--- a/src/frontends/android/app/build.gradle
+++ b/src/frontends/android/app/build.gradle
@@ -34,9 +34,14 @@ android {
buildTypes {
release {
- minifyEnabled false
+ minifyEnabled = true
+ shrinkResources = true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt'
}
+ releaseTest {
+ initWith release
+ signingConfig = signingConfigs.debug
+ }
}
compileOptions {
targetCompatibility 1.8
diff --git a/src/frontends/android/app/proguard-rules.txt b/src/frontends/android/app/proguard-rules.txt
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java
index 0f1dfdc4b3..2fa51c0f0f 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java
@@ -18,7 +18,6 @@
package org.strongswan.android.logic;
-import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -73,12 +72,12 @@ import java.security.PrivateKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.SortedSet;
+import androidx.annotation.Keep;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
@@ -640,6 +639,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
*
* @param status new state
*/
+ @Keep
public void updateStatus(int status)
{
switch (status)
@@ -683,6 +683,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
*
* @param value new state
*/
+ @Keep
public void updateImcState(int value)
{
ImcState state = ImcState.fromValue(value);
@@ -698,6 +699,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
*
* @param xml XML text
*/
+ @Keep
public void addRemediationInstruction(String xml)
{
for (RemediationInstruction instruction : RemediationInstruction.fromXml(xml))
@@ -718,6 +720,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
*
* @return a list of DER encoded CA certificates
*/
+ @Keep
private byte[][] getTrustedCertificates()
{
ArrayList<byte[]> certs = new ArrayList<byte[]>();
@@ -762,6 +765,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
* @throws KeyChainException
* @throws CertificateEncodingException
*/
+ @Keep
private byte[][] getUserCertificate() throws KeyChainException, InterruptedException, CertificateEncodingException
{
ArrayList<byte[]> encodings = new ArrayList<byte[]>();
@@ -787,6 +791,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
* @throws InterruptedException
* @throws KeyChainException
*/
+ @Keep
private PrivateKey getUserKey() throws KeyChainException, InterruptedException
{
return KeyChain.getPrivateKey(getApplicationContext(), mCurrentUserCertificateAlias);
@@ -818,6 +823,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
* Adapter for VpnService.Builder which is used to access it safely via JNI.
* There is a corresponding C object to access it from native code.
*/
+ @Keep
public class BuilderAdapter
{
private VpnProfile mProfile;
@@ -1398,6 +1404,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
/**
* Function called via JNI to determine information about the Android version.
*/
+ @Keep
private static String getAndroidVersion()
{
return "Android " + Build.VERSION.RELEASE + " - " + Build.DISPLAY + "/" + Build.VERSION.SECURITY_PATCH;
@@ -1406,6 +1413,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe
/**
* Function called via JNI to determine information about the device.
*/
+ @Keep
private static String getDeviceString()
{
return Build.MODEL + " - " + Build.BRAND + "/" + Build.PRODUCT + "/" + Build.MANUFACTURER;
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java
index 2dccf496ec..4166df846d 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java
@@ -33,8 +33,10 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
+import androidx.annotation.Keep;
import androidx.annotation.NonNull;
+@Keep
public class NetworkManager extends BroadcastReceiver implements Runnable
{
private final Context mContext;
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/Scheduler.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/Scheduler.java
index 47714b1f20..51fe098f7f 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/Scheduler.java
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/Scheduler.java
@@ -29,6 +29,9 @@ import java.util.ArrayList;
import java.util.PriorityQueue;
import java.util.UUID;
+import androidx.annotation.Keep;
+
+@Keep
public class Scheduler extends BroadcastReceiver
{
private final String EXECUTE_JOB = "org.strongswan.android.Scheduler.EXECUTE_JOB";
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/AndroidImc.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/AndroidImc.java
index c3acbc3b42..53f3c260bb 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/AndroidImc.java
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/imc/AndroidImc.java
@@ -28,6 +28,9 @@ import org.strongswan.android.logic.imc.collectors.StringVersionCollector;
import android.content.Context;
+import androidx.annotation.Keep;
+
+@Keep
public class AndroidImc
{
private final Context mContext;
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java b/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java
index 3ef0ebab1f..f1c5c19377 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreProvider.java
@@ -18,6 +18,9 @@ package org.strongswan.android.security;
import java.security.Provider;
+import androidx.annotation.Keep;
+
+@Keep
public class LocalCertificateKeyStoreProvider extends Provider
{
private static final long serialVersionUID = 3515038332469843219L;
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java b/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java
index b3b665138d..690a1ae6e1 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateKeyStoreSpi.java
@@ -30,6 +30,9 @@ import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
+import androidx.annotation.Keep;
+
+@Keep
public class LocalCertificateKeyStoreSpi extends KeyStoreSpi
{
private final LocalCertificateStore mStore = new LocalCertificateStore();
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateStore.java b/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateStore.java
index 133a78ca6c..85362b1257 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateStore.java
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/security/LocalCertificateStore.java
@@ -37,6 +37,9 @@ import org.strongswan.android.utils.Utils;
import android.content.Context;
+import androidx.annotation.Keep;
+
+@Keep
public class LocalCertificateStore
{
private static final String FILE_PREFIX = "certificate-";
diff --git a/src/frontends/android/gradle.properties b/src/frontends/android/gradle.properties
index dd1c0d5138..2e22173031 100644
--- a/src/frontends/android/gradle.properties
+++ b/src/frontends/android/gradle.properties
@@ -1,6 +1,5 @@
android.dependency.useConstraints=false
android.enableJetifier=false
android.nonTransitiveRClass=false
-android.r8.strictFullModeForKeepRules=false
android.uniquePackageNames=false
android.useAndroidX=true