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.
209 lines
8.6 KiB
C
209 lines
8.6 KiB
C
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <jni.h>
|
|
#include <string.h>
|
|
#include "jni_android.h"
|
|
#include "_cgo_export.h"
|
|
|
|
// Global context
|
|
static jobject ctx = NULL;
|
|
|
|
// g_view: global ref to the GioView, set in registerFragment. Used by
|
|
// SetGestureExclusions to reach View.setSystemGestureExclusionRects.
|
|
static jobject g_view = NULL;
|
|
|
|
void
|
|
registerFragment(JNIEnv *env, jobject 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;");
|
|
jobject l_ctx = (*env)->CallObjectMethod(env, view, mid);
|
|
ctx = (*env)->NewGlobalRef(env, l_ctx);
|
|
cls = (*env)->GetObjectClass(env, ctx);
|
|
mid = (*env)->GetMethodID(env, cls, "getClassLoader", "()Ljava/lang/ClassLoader;");
|
|
jobject loader = (*env)->CallObjectMethod(env, ctx, mid);
|
|
cls = (*env)->GetObjectClass(env, loader);
|
|
mid = (*env)->GetMethodID(env, cls, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
|
jstring str = (*env)->NewStringUTF(env, "st/wow/git/logbook/Permissions");
|
|
cls = (*env)->CallObjectMethod(env, loader, mid, str);
|
|
mid = (*env)->GetMethodID(env, cls, "<init>", "(Landroid/view/View;)V");
|
|
jobject inst = (*env)->NewObject(env, cls, mid, view);
|
|
}
|
|
|
|
void CallVoidMethod(JNIEnv *env, jobject obj, jmethodID methodID) {
|
|
(*env)->CallVoidMethod(env, obj, methodID);
|
|
}
|
|
|
|
jint GetEnv(JavaVM *vm, JNIEnv **env, jint version) {
|
|
return (*vm)->GetEnv(vm, (void **)env, version);
|
|
}
|
|
|
|
jint AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) {
|
|
return (*vm)->AttachCurrentThread(vm, p_env, thr_args);
|
|
}
|
|
|
|
jint DetachCurrentThread(JavaVM *vm) {
|
|
return (*vm)->DetachCurrentThread(vm);
|
|
}
|
|
|
|
jobject
|
|
NewGlobalRef(JNIEnv *env, jobject o) {
|
|
return (*env)->NewGlobalRef(env, o);
|
|
}
|
|
|
|
|
|
|
|
// Open file in Termux's file editor via ACTION_SEND share intent.
|
|
// env: JNIEnv* from your JNI callback
|
|
// path: absolute filesystem path to the file (e.g. "/storage/emulated/0/foo.txt")
|
|
void open_file_in_termux(JNIEnv *env, const char *path) {
|
|
if (!ctx) return;
|
|
|
|
jclass intentClass = (*env)->FindClass(env, "android/content/Intent");
|
|
if (!intentClass) return;
|
|
|
|
// new Intent("android.intent.action.SEND")
|
|
jmethodID intentInit = (*env)->GetMethodID(env, intentClass, "<init>", "(Ljava/lang/String;)V");
|
|
if (!intentInit) return;
|
|
|
|
jstring actionSend = (*env)->NewStringUTF(env, "android.intent.action.SEND");
|
|
jobject intent = (*env)->NewObject(env, intentClass, intentInit, actionSend);
|
|
if (!intent) return;
|
|
|
|
// intent.setType("text/plain");
|
|
jmethodID setType = (*env)->GetMethodID(env, intentClass,
|
|
"setType", "(Ljava/lang/String;)Landroid/content/Intent;");
|
|
if (!setType) return;
|
|
|
|
jstring type = (*env)->NewStringUTF(env, "text/plain");
|
|
(*env)->CallObjectMethod(env, intent, setType, type);
|
|
|
|
// Uri uri = Uri.parse("file://" + path);
|
|
jclass uriClass = (*env)->FindClass(env, "android/net/Uri");
|
|
if (!uriClass) return;
|
|
|
|
jmethodID uriParse = (*env)->GetStaticMethodID(env, uriClass,
|
|
"parse", "(Ljava/lang/String;)Landroid/net/Uri;");
|
|
if (!uriParse) return;
|
|
|
|
// Build "file://" + path
|
|
size_t uriLen = strlen("file://") + strlen(path) + 1;
|
|
char *uriStr = (char *)malloc(uriLen);
|
|
if (!uriStr) return;
|
|
strcpy(uriStr, "file://");
|
|
strcat(uriStr, path);
|
|
|
|
jstring uriJstr = (*env)->NewStringUTF(env, uriStr);
|
|
jobject uri = (*env)->CallStaticObjectMethod(env, uriClass, uriParse, uriJstr);
|
|
free(uriStr);
|
|
|
|
if (!uri) return;
|
|
|
|
// intent.setData(uri);
|
|
jmethodID setData = (*env)->GetMethodID(env, intentClass,
|
|
"setData", "(Landroid/net/Uri;)Landroid/content/Intent;");
|
|
if (!setData) return;
|
|
|
|
(*env)->CallObjectMethod(env, intent, setData, uri);
|
|
|
|
// intent.putExtra("android.intent.extra.STREAM", uri.toString());
|
|
// Use putExtra(String, String)
|
|
jmethodID putExtra = (*env)->GetMethodID(env, intentClass,
|
|
"putExtra", "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;");
|
|
if (!putExtra) return;
|
|
|
|
jstring extraKey = (*env)->NewStringUTF(env, "android.intent.extra.STREAM");
|
|
(*env)->CallObjectMethod(env, intent, putExtra, extraKey, uriJstr);
|
|
|
|
// ctx.startActivity(intent);
|
|
jclass contextClass = (*env)->GetObjectClass(env, ctx);
|
|
jmethodID startActivity = (*env)->GetMethodID(env, contextClass,
|
|
"startActivity", "(Landroid/content/Intent;)V");
|
|
if (!startActivity) return;
|
|
|
|
(*env)->CallVoidMethod(env, ctx, startActivity, intent);
|
|
}
|
|
|
|
// Set the exclusion rects ON THE UI THREAD. This function is called from the
|
|
// Go frame loop, which runs on its own OS thread (NOT the Android UI thread
|
|
// — the JNI event pump merely enqueues events that the app goroutine drains).
|
|
// setSystemGestureExclusionRects stores the rects on the view and triggers a
|
|
// relayout; it is the RELAYOUT that reports them to the window manager, which
|
|
// owns the edge back-gesture zone. A direct cross-thread call leaves the
|
|
// window manager unaware, so the rects are applied through
|
|
// view.post(PadExcl) — a Runnable (org.gioui.PadExcl, added by the build
|
|
// scripts' smali patch) that runs on the UI thread.
|
|
void SetGestureExclusions(JNIEnv *env, jint nrects, const jint *xyxy) {
|
|
if (g_view == NULL) return;
|
|
jclass verCls = (*env)->FindClass(env, "android/os/Build$VERSION");
|
|
if (verCls == NULL) return;
|
|
jfieldID sdkF = (*env)->GetStaticFieldID(env, verCls, "SDK_INT", "I");
|
|
if (sdkF == NULL) return;
|
|
if ((*env)->GetStaticIntField(env, verCls, sdkF) < 29) return;
|
|
jclass rectCls = (*env)->FindClass(env, "android/graphics/Rect");
|
|
jmethodID rectInit = (*env)->GetMethodID(env, rectCls, "<init>", "(IIII)V");
|
|
jclass listCls = (*env)->FindClass(env, "java/util/ArrayList");
|
|
jmethodID listInit = (*env)->GetMethodID(env, listCls, "<init>", "()V");
|
|
jobject list = (*env)->NewObject(env, listCls, listInit);
|
|
if (list == NULL) return;
|
|
jmethodID addM = (*env)->GetMethodID(env, listCls, "add", "(Ljava/lang/Object;)Z");
|
|
for (int i = 0; i < nrects; i++) {
|
|
jobject rect = (*env)->NewObject(env, rectCls, rectInit,
|
|
xyxy[i * 4], xyxy[i * 4 + 1], xyxy[i * 4 + 2], xyxy[i * 4 + 3]);
|
|
if (rect == NULL) continue;
|
|
(*env)->CallBooleanMethod(env, list, addM, rect);
|
|
(*env)->DeleteLocalRef(env, rect);
|
|
}
|
|
// Find the app class through the context's class loader. A bare
|
|
// FindClass from this (non-Java) thread resolves against the BOOT
|
|
// class loader only and would never find org.gioui.PadExcl.
|
|
jclass ctxCls = (*env)->GetObjectClass(env, ctx);
|
|
jmethodID gclM = (*env)->GetMethodID(env, ctxCls, "getClassLoader", "()Ljava/lang/ClassLoader;");
|
|
jobject loader = (*env)->CallObjectMethod(env, ctx, gclM);
|
|
jclass loaderCls = (*env)->GetObjectClass(env, loader);
|
|
jmethodID fcM = (*env)->GetMethodID(env, loaderCls, "findClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
|
jstring clsName = (*env)->NewStringUTF(env, "org.gioui.PadExcl");
|
|
jclass padExclCls = (jclass)(*env)->CallObjectMethod(env, loader, fcM, clsName);
|
|
if (padExclCls == NULL) {
|
|
(*env)->ExceptionClear(env);
|
|
(*env)->DeleteLocalRef(env, list);
|
|
return;
|
|
}
|
|
jmethodID padExclInit = (*env)->GetMethodID(env, padExclCls, "<init>", "(Landroid/view/View;Ljava/util/List;)V");
|
|
jobject runnable = (*env)->NewObject(env, padExclCls, padExclInit, g_view, list);
|
|
if (runnable == NULL) {
|
|
(*env)->ExceptionClear(env);
|
|
(*env)->DeleteLocalRef(env, list);
|
|
return;
|
|
}
|
|
jmethodID postM = (*env)->GetMethodID(env, (*env)->GetObjectClass(env, g_view),
|
|
"post", "(Ljava/lang/Runnable;)Z");
|
|
(void)(*env)->CallBooleanMethod(env, g_view, postM, runnable);
|
|
(*env)->ExceptionClear(env);
|
|
(*env)->DeleteLocalRef(env, runnable);
|
|
(*env)->DeleteLocalRef(env, list);
|
|
}
|
|
|
|
// Java_org_gioui_GioActivity_padFlushSession
|
|
//
|
|
// Called from GioActivity.onStop (injected by the build script): persist
|
|
// the session snapshot before the process is likely killed. The Go side
|
|
// (pad_flush_session) only enqueues a request on the logic goroutine's
|
|
// channel, so this returns immediately on the UI thread.
|
|
void
|
|
Java_org_gioui_GioActivity_padFlushSession(JNIEnv *env, jclass cls) {
|
|
pad_flush_session();
|
|
}
|