From fdbffc9f5c7b43d0a7f6d889df18b2f9ba65ebd5 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Thu, 20 Aug 2026 18:49:00 -0400 Subject: [PATCH] Fix SIGABRT on activity destroy: null view in registerFragment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crash on device (Pixel 9 Pro, Android 17) when swiping the app away from recents: JNI DETECTED ERROR: java_object == null in call to GetObjectClass #06 libgio.so (registerFragment+104) Root cause: Gio's window.detach sends an EMPTY AndroidViewEvent (View == 0) as its detach signal (os_android.go: window.detach -> processEvent(AndroidViewEvent{})), which fires when the GioView is destroyed — i.e. the activity going away on a recents-wipe. Our handleEvent passed that null ref straight into registerFragment, whose GetObjectClass(null) aborts. Pre-existing latent bug; the emulator never delivered a detach event in testing (home keeps the view attached, force-stop kills before the event dispatches). Guard on both sides: handleEvent ignores the View == 0 detach signal (a re-attach arrives as a fresh event with a live view), and registerFragment returns early on a null view as defense in depth. Verified on the crashing device: recents swipe now closes the app cleanly, crash buffer empty. --- cmd/pad/impl_android.go | 9 +++++++++ cmd/pad/jni_android.c | 18 +++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/cmd/pad/impl_android.go b/cmd/pad/impl_android.go index 442cb5a..adf71ae 100644 --- a/cmd/pad/impl_android.go +++ b/cmd/pad/impl_android.go @@ -41,6 +41,15 @@ func impl_start() {} func handleEvent(e event.Event) { switch e := e.(type) { case app.AndroidViewEvent: + // View == 0 is Gio's DETACH signal (window.detach sends an empty + // AndroidViewEvent when the GioView is destroyed — e.g. the activity + // going away on a recents-wipe). There is nothing to register for a + // detached view; passing the null ref on would abort in JNI + // (GetObjectClass on null). A re-attach arrives as a fresh event + // with a live view. + if e.View == 0 { + return + } theJVM = (*C.JavaVM)(unsafe.Pointer(app.JavaVM())) RunInJVM(func(env *JNIEnv) { C.registerFragment(env, (C.jobject)(unsafe.Pointer(e.View))) diff --git a/cmd/pad/jni_android.c b/cmd/pad/jni_android.c index 3ca7b9f..df77efb 100644 --- a/cmd/pad/jni_android.c +++ b/cmd/pad/jni_android.c @@ -15,13 +15,17 @@ static jobject g_view = NULL; void registerFragment(JNIEnv *env, jobject view) { - if (view != NULL) { - if (g_view == NULL) { - g_view = (*env)->NewGlobalRef(env, view); - } else if (g_view != view) { - (*env)->DeleteGlobalRef(env, g_view); - g_view = (*env)->NewGlobalRef(env, view); - } + if (view == NULL) { + // Detach signal (Gio sends an empty AndroidViewEvent when the + // view is destroyed). The Go side already filters these; this + // guard keeps the JNI calls safe if one ever slips through. + return; + } + if (g_view == NULL) { + g_view = (*env)->NewGlobalRef(env, view); + } else if (g_view != view) { + (*env)->DeleteGlobalRef(env, g_view); + g_view = (*env)->NewGlobalRef(env, view); } jclass cls = (*env)->GetObjectClass(env, view); jmethodID mid = (*env)->GetMethodID(env, cls, "getContext", "()Landroid/content/Context;");