From 2b3f20d3a02ec585876d2d6d200d1b8728d5d552 Mon Sep 17 00:00:00 2001 From: Mikkeli Date: Wed, 26 Aug 2026 22:43:35 +0900 Subject: [PATCH] fix(android): keep the JNI callback names through R8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release builds aborted on the first callback of every test: NoSuchMethodError: no non-static method "Lp0;.onOutput(Ljava/lang/String;)V" R8 had renamed MainThreadCallback to p0 along with its methods, so GetMethodID(env, cls, "onOutput", ...) in iperf_jni.c resolved nothing. The names are a contract between C and Kotlin that the minifier cannot see. Adds keep rules for IperfRunner, the IperfCallback interface and — the part that matters — everything implementing it, since the lookup happens on the concrete object's class rather than on the interface. A general rule keeps any class declaring a native method, so a future one is covered without a new rule. Debug builds do not minify, which is why every on-device check so far passed while the shipping artifact was broken. Found by running the release APK during release preparation. Co-Authored-By: Claude Opus 5 --- android/app/build.gradle.kts | 7 +++++++ android/app/proguard-rules.pro | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 android/app/proguard-rules.pro diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 04af821..6a15324 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -37,6 +37,13 @@ android { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("debug") + + // proguard-rules.pro keeps the names JNI resolves at runtime. + // Without it a release build aborts on the first callback. + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) } } diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..9f188aa --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# JNI reaches these by name. R8 renames by default, so without these rules a +# release build resolves nothing: GetMethodID("onOutput", ...) returns NULL, +# raises NoSuchMethodError, and the next JNI call aborts the process with +# NoSuchMethodError: no non-static method "Lp0;.onOutput(Ljava/lang/String;)V" +# Debug builds do not minify, so this only ever appears in release. + +# The plugin object holds the native method declarations and is looked up from +# C by its fully qualified name (Java_com_novoyuuparosk_iperf3gui_IperfRunner_*). +-keep class com.novoyuuparosk.iperf3gui.IperfRunner { *; } + +# The callback interface and everything implementing it: iperf_jni.c resolves +# onOutput/onError/onComplete by name on the concrete object's class, so the +# implementors' method names must survive too, not just the interface's. +-keep interface com.novoyuuparosk.iperf3gui.IperfRunner$IperfCallback { *; } +-keep class * implements com.novoyuuparosk.iperf3gui.IperfRunner$IperfCallback { *; } + +# Belt and braces for any future native method: keep the name of any class +# declaring one, and the native methods themselves. +-keepclasseswithmembernames,includedescriptorclasses class * { + native ; +}