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 ; +} diff --git a/android/app/src/main/cpp/iperf_jni.c b/android/app/src/main/cpp/iperf_jni.c index ec6dca4..caae63c 100644 --- a/android/app/src/main/cpp/iperf_jni.c +++ b/android/app/src/main/cpp/iperf_jni.c @@ -86,6 +86,21 @@ struct iperf_run_state { * iperf_run_client() and cleared after it returns. */ static struct iperf_run_state *g_current_state = NULL; +/* Resolve a method ID, clearing the exception a failed lookup leaves behind. + * + * GetMethodID raises NoSuchMethodError on failure, and any further JNI call + * made while an exception is pending aborts the process. Returning NULL and + * clearing lets the caller fail on its own terms. */ +static jmethodID resolve_method(JNIEnv *env, jclass cls, + const char *name, const char *sig) { + jmethodID mid = (*env)->GetMethodID(env, cls, name, sig); + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + return NULL; + } + return mid; +} + /* Helper: invoke the output callback via CallVoidMethod */ static void signal_output(JNIEnv *env, struct iperf_run_state *state, const char *msg) { @@ -272,13 +287,25 @@ Java_com_novoyuuparosk_iperf3gui_IperfRunner_runIperfLive( /* Resolve callback method IDs up front so failures can be reported. */ jclass callback_cls = (*env)->GetObjectClass(env, callback); - jmethodID mid_output = (*env)->GetMethodID( + jmethodID mid_output = resolve_method( env, callback_cls, "onOutput", "(Ljava/lang/String;)V"); - jmethodID mid_error = (*env)->GetMethodID( + jmethodID mid_error = resolve_method( env, callback_cls, "onError", "(Ljava/lang/String;)V"); - jmethodID mid_complete = (*env)->GetMethodID( + jmethodID mid_complete = resolve_method( env, callback_cls, "onComplete", "()V"); + /* If the names did not resolve there is no way to report anything: the + * reporting path is the thing that is missing. Bail out loudly instead. + * + * In practice this means R8 renamed the callback in a release build and + * android/app/proguard-rules.pro has stopped covering it. */ + if (!mid_output || !mid_error || !mid_complete) { + LOGE("Callback methods did not resolve (onOutput=%p onError=%p " + "onComplete=%p) — check the -keep rules for IperfCallback", + mid_output, mid_error, mid_complete); + return; + } + /* Report a startup failure directly on the callback object and finish, * so Dart's completer always resolves instead of waiting for a timeout. */ #define FAIL_EARLY(msg) do { \