// SPDX-License-Identifier: Unlicense OR MIT package wm /* #cgo CFLAGS: -Werror #cgo LDFLAGS: -landroid #include #include #include #include #include __attribute__ ((visibility ("hidden"))) jint gio_jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version); __attribute__ ((visibility ("hidden"))) jint gio_jni_GetJavaVM(JNIEnv *env, JavaVM **jvm); __attribute__ ((visibility ("hidden"))) jint gio_jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args); __attribute__ ((visibility ("hidden"))) jint gio_jni_DetachCurrentThread(JavaVM *vm); __attribute__ ((visibility ("hidden"))) jobject gio_jni_NewGlobalRef(JNIEnv *env, jobject obj); __attribute__ ((visibility ("hidden"))) void gio_jni_DeleteGlobalRef(JNIEnv *env, jobject obj); __attribute__ ((visibility ("hidden"))) jclass gio_jni_GetObjectClass(JNIEnv *env, jobject obj); __attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); __attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); __attribute__ ((visibility ("hidden"))) jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID); __attribute__ ((visibility ("hidden"))) jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID); __attribute__ ((visibility ("hidden"))) void gio_jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args); __attribute__ ((visibility ("hidden"))) void gio_jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); __attribute__ ((visibility ("hidden"))) jbyte *gio_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr); __attribute__ ((visibility ("hidden"))) void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes); __attribute__ ((visibility ("hidden"))) jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr); __attribute__ ((visibility ("hidden"))) jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len); __attribute__ ((visibility ("hidden"))) jsize gio_jni_GetStringLength(JNIEnv *env, jstring str); __attribute__ ((visibility ("hidden"))) const jchar *gio_jni_GetStringChars(JNIEnv *env, jstring str); __attribute__ ((visibility ("hidden"))) jthrowable gio_jni_ExceptionOccurred(JNIEnv *env); __attribute__ ((visibility ("hidden"))) void gio_jni_ExceptionClear(JNIEnv *env); __attribute__ ((visibility ("hidden"))) jobject gio_jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args); __attribute__ ((visibility ("hidden"))) jobject gio_jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args); */ import "C" import ( "errors" "fmt" "image" "reflect" "runtime" "runtime/debug" "sync" "time" "unicode/utf16" "unsafe" "github.com/p9c/p9/pkg/gel/gio/f32" "github.com/p9c/p9/pkg/gel/gio/io/clipboard" "github.com/p9c/p9/pkg/gel/gio/io/key" "github.com/p9c/p9/pkg/gel/gio/io/pointer" "github.com/p9c/p9/pkg/gel/gio/io/system" "github.com/p9c/p9/pkg/gel/gio/unit" ) type window struct { callbacks Callbacks view C.jobject dpi int fontScale float32 insets system.Insets stage system.Stage started bool state, newState windowState // mu protects the fields following it. mu sync.Mutex win *C.ANativeWindow animating bool } // windowState tracks the View or Activity specific state lost when Android // re-creates our Activity. type windowState struct { cursor *pointer.CursorName } // gioView hold cached JNI methods for GioView. var gioView struct { once sync.Once getDensity C.jmethodID getFontScale C.jmethodID showTextInput C.jmethodID hideTextInput C.jmethodID postFrameCallback C.jmethodID setCursor C.jmethodID } // ViewEvent is sent whenever the Window's underlying Android view // changes. type ViewEvent struct { // View is a JNI global reference to the android.view.View // instance backing the Window. The reference is valid until // the next ViewEvent is received. // A zero View means that there is currently no view attached. View uintptr } type jvalue uint64 // The largest JNI type fits in 64 bits. var dataDirChan = make(chan string, 1) var android struct { // mu protects all fields of this structure. However, once a // non-nil jvm is returned from javaVM, all the other fields may // be accessed unlocked. mu sync.Mutex jvm *C.JavaVM // appCtx is the global Android App context. appCtx C.jobject // gioCls is the class of the Gio class. gioCls C.jclass mwriteClipboard C.jmethodID mreadClipboard C.jmethodID mwakeupMainThread C.jmethodID } // view maps from GioView JNI refenreces to windows. var views = make(map[C.jlong]*window) // windows maps from Callbacks to windows var windows = make(map[Callbacks]*window) var mainWindow = newWindowRendezvous() var mainFuncs = make(chan func(env *C.JNIEnv), 1) func getMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID { m := C.CString(method) defer C.free(unsafe.Pointer(m)) s := C.CString(sig) defer C.free(unsafe.Pointer(s)) jm := C.gio_jni_GetMethodID(env, class, m, s) if err := exception(env); err != nil { panic(err) } return jm } func getStaticMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID { m := C.CString(method) defer C.free(unsafe.Pointer(m)) s := C.CString(sig) defer C.free(unsafe.Pointer(s)) jm := C.gio_jni_GetStaticMethodID(env, class, m, s) if err := exception(env); err != nil { panic(err) } return jm } //export Java_org_gioui_Gio_runGoMain func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass, jdataDir C.jbyteArray, context C.jobject) { initJVM(env, class, context) dirBytes := C.gio_jni_GetByteArrayElements(env, jdataDir) if dirBytes == nil { panic("runGoMain: GetByteArrayElements failed") } n := C.gio_jni_GetArrayLength(env, jdataDir) dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n) dataDirChan <- dataDir C.gio_jni_ReleaseByteArrayElements(env, jdataDir, dirBytes) runMain() } func initJVM(env *C.JNIEnv, gio C.jclass, ctx C.jobject) { android.mu.Lock() defer android.mu.Unlock() if res := C.gio_jni_GetJavaVM(env, &android.jvm); res != 0 { panic("gio: GetJavaVM failed") } android.appCtx = C.gio_jni_NewGlobalRef(env, ctx) android.gioCls = C.jclass(C.gio_jni_NewGlobalRef(env, C.jobject(gio))) android.mwriteClipboard = getStaticMethodID(env, gio, "writeClipboard", "(Landroid/content/Context;Ljava/lang/String;)V") android.mreadClipboard = getStaticMethodID(env, gio, "readClipboard", "(Landroid/content/Context;)Ljava/lang/String;") android.mwakeupMainThread = getStaticMethodID(env, gio, "wakeupMainThread", "()V") } func JavaVM() uintptr { jvm := javaVM() return uintptr(unsafe.Pointer(jvm)) } func javaVM() *C.JavaVM { android.mu.Lock() defer android.mu.Unlock() return android.jvm } func AppContext() uintptr { android.mu.Lock() defer android.mu.Unlock() return uintptr(android.appCtx) } func GetDataDir() string { return <-dataDirChan } //export Java_org_gioui_GioView_onCreateView func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.jobject) C.jlong { gioView.once.Do(func() { m := &gioView m.getDensity = getMethodID(env, class, "getDensity", "()I") m.getFontScale = getMethodID(env, class, "getFontScale", "()F") m.showTextInput = getMethodID(env, class, "showTextInput", "()V") m.hideTextInput = getMethodID(env, class, "hideTextInput", "()V") m.postFrameCallback = getMethodID(env, class, "postFrameCallback", "()V") m.setCursor = getMethodID(env, class, "setCursor", "(I)V") }) view = C.gio_jni_NewGlobalRef(env, view) wopts := <-mainWindow.out w, ok := windows[wopts.window] if !ok { w = &window{ callbacks: wopts.window, } windows[wopts.window] = w } w.callbacks.SetDriver(w) w.view = view handle := C.jlong(view) views[handle] = w w.loadConfig(env, class) applyStateDiff(env, view, windowState{}, w.state) w.setStage(system.StagePaused) w.callbacks.Event(ViewEvent{View: uintptr(view)}) return handle } //export Java_org_gioui_GioView_onDestroyView func Java_org_gioui_GioView_onDestroyView(env *C.JNIEnv, class C.jclass, handle C.jlong) { w := views[handle] w.callbacks.Event(ViewEvent{View: 0}) w.callbacks.SetDriver(nil) delete(views, handle) C.gio_jni_DeleteGlobalRef(env, w.view) w.view = 0 } //export Java_org_gioui_GioView_onStopView func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass, handle C.jlong) { w := views[handle] w.started = false w.setStage(system.StagePaused) } //export Java_org_gioui_GioView_onStartView func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, handle C.jlong) { w := views[handle] w.started = true if w.aNativeWindow() != nil { w.setVisible() } } //export Java_org_gioui_GioView_onSurfaceDestroyed func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) { w := views[handle] w.mu.Lock() w.win = nil w.mu.Unlock() w.setStage(system.StagePaused) } //export Java_org_gioui_GioView_onSurfaceChanged func Java_org_gioui_GioView_onSurfaceChanged(env *C.JNIEnv, class C.jclass, handle C.jlong, surf C.jobject) { w := views[handle] w.mu.Lock() w.win = C.ANativeWindow_fromSurface(env, surf) w.mu.Unlock() if w.started { w.setVisible() } } //export Java_org_gioui_GioView_onLowMemory func Java_org_gioui_GioView_onLowMemory() { runtime.GC() debug.FreeOSMemory() } //export Java_org_gioui_GioView_onConfigurationChanged func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv, class C.jclass, view C.jlong) { w := views[view] w.loadConfig(env, class) if w.stage >= system.StageRunning { w.draw(true) } } //export Java_org_gioui_GioView_onFrameCallback func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view C.jlong, nanos C.jlong) { w, exist := views[view] if !exist { return } if w.stage < system.StageRunning { return } w.mu.Lock() anim := w.animating w.mu.Unlock() if anim { runInJVM(javaVM(), func(env *C.JNIEnv) { callVoidMethod(env, w.view, gioView.postFrameCallback) }) w.draw(false) } } //export Java_org_gioui_GioView_onBack func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean { w := views[view] ev := &system.CommandEvent{Type: system.CommandBack} w.callbacks.Event(ev) if ev.Cancel { return C.JNI_TRUE } return C.JNI_FALSE } //export Java_org_gioui_GioView_onFocusChange func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) { w := views[view] w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE}) } //export Java_org_gioui_GioView_onWindowInsets func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C.jlong, top, right, bottom, left C.jint) { w := views[view] w.insets = system.Insets{ Top: unit.Px(float32(top)), Right: unit.Px(float32(right)), Bottom: unit.Px(float32(bottom)), Left: unit.Px(float32(left)), } if w.stage >= system.StageRunning { w.draw(true) } } func (w *window) setVisible() { win := w.aNativeWindow() width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) if width == 0 || height == 0 { return } w.setStage(system.StageRunning) w.draw(true) } func (w *window) setStage(stage system.Stage) { if stage == w.stage { return } w.stage = stage w.callbacks.Event(system.StageEvent{stage}) } func (w *window) nativeWindow(visID int) (*C.ANativeWindow, int, int) { win := w.aNativeWindow() var width, height int if win != nil { if C.ANativeWindow_setBuffersGeometry(win, 0, 0, C.int32_t(visID)) != 0 { panic(errors.New("ANativeWindow_setBuffersGeometry failed")) } w, h := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) width, height = int(w), int(h) } return win, width, height } func (w *window) aNativeWindow() *C.ANativeWindow { w.mu.Lock() defer w.mu.Unlock() return w.win } func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) { dpi := int(C.gio_jni_CallIntMethod(env, w.view, gioView.getDensity)) w.fontScale = float32(C.gio_jni_CallFloatMethod(env, w.view, gioView.getFontScale)) switch dpi { case C.ACONFIGURATION_DENSITY_NONE, C.ACONFIGURATION_DENSITY_DEFAULT, C.ACONFIGURATION_DENSITY_ANY: // Assume standard density. w.dpi = C.ACONFIGURATION_DENSITY_MEDIUM default: w.dpi = int(dpi) } } func (w *window) SetAnimating(anim bool) { w.mu.Lock() w.animating = anim w.mu.Unlock() if anim { runOnMain(func(env *C.JNIEnv) { if w.view == 0 { // View was destroyed while switching to main thread. return } callVoidMethod(env, w.view, gioView.postFrameCallback) }) } } func (w *window) draw(sync bool) { win := w.aNativeWindow() width, height := C.ANativeWindow_getWidth(win), C.ANativeWindow_getHeight(win) if width == 0 || height == 0 { return } const inchPrDp = 1.0 / 160 ppdp := float32(w.dpi) * inchPrDp w.callbacks.Event(FrameEvent{ FrameEvent: system.FrameEvent{ Now: time.Now(), Size: image.Point{ X: int(width), Y: int(height), }, Insets: w.insets, Metric: unit.Metric{ PxPerDp: ppdp, PxPerSp: w.fontScale * ppdp, }, }, Sync: sync, }) } type keyMapper func(devId, keyCode C.int32_t) rune func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) { if jvm == nil { panic("nil JVM") } runtime.LockOSThread() defer runtime.UnlockOSThread() var env *C.JNIEnv if res := C.gio_jni_GetEnv(jvm, &env, C.JNI_VERSION_1_6); res != C.JNI_OK { if res != C.JNI_EDETACHED { panic(fmt.Errorf("JNI GetEnv failed with error %d", res)) } if C.gio_jni_AttachCurrentThread(jvm, &env, nil) != C.JNI_OK { panic(errors.New("runInJVM: AttachCurrentThread failed")) } defer C.gio_jni_DetachCurrentThread(jvm) } f(env) } func convertKeyCode(code C.jint) (string, bool) { var n string switch code { case C.AKEYCODE_DPAD_UP: n = key.NameUpArrow case C.AKEYCODE_DPAD_DOWN: n = key.NameDownArrow case C.AKEYCODE_DPAD_LEFT: n = key.NameLeftArrow case C.AKEYCODE_DPAD_RIGHT: n = key.NameRightArrow case C.AKEYCODE_FORWARD_DEL: n = key.NameDeleteForward case C.AKEYCODE_DEL: n = key.NameDeleteBackward case C.AKEYCODE_NUMPAD_ENTER: n = key.NameEnter case C.AKEYCODE_ENTER: n = key.NameEnter default: return "", false } return n, true } //export Java_org_gioui_GioView_onKeyEvent func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, keyCode, r C.jint, t C.jlong) { w := views[handle] if n, ok := convertKeyCode(keyCode); ok { w.callbacks.Event(key.Event{Name: n}) } if r != 0 { w.callbacks.Event(key.EditEvent{Text: string(rune(r))}) } } //export Java_org_gioui_GioView_onTouchEvent func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, action, pointerID, tool C.jint, x, y, scrollX, scrollY C.jfloat, jbtns C.jint, t C.jlong) { w := views[handle] var typ pointer.Type switch action { case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN: typ = pointer.Press case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP: typ = pointer.Release case C.AMOTION_EVENT_ACTION_CANCEL: typ = pointer.Cancel case C.AMOTION_EVENT_ACTION_MOVE: typ = pointer.Move case C.AMOTION_EVENT_ACTION_SCROLL: typ = pointer.Scroll default: return } var src pointer.Source var btns pointer.Buttons if jbtns&C.AMOTION_EVENT_BUTTON_PRIMARY != 0 { btns |= pointer.ButtonPrimary } if jbtns&C.AMOTION_EVENT_BUTTON_SECONDARY != 0 { btns |= pointer.ButtonSecondary } if jbtns&C.AMOTION_EVENT_BUTTON_TERTIARY != 0 { btns |= pointer.ButtonTertiary } switch tool { case C.AMOTION_EVENT_TOOL_TYPE_FINGER: src = pointer.Touch case C.AMOTION_EVENT_TOOL_TYPE_MOUSE: src = pointer.Mouse case C.AMOTION_EVENT_TOOL_TYPE_UNKNOWN: // For example, triggered via 'adb shell input tap'. // Instead of discarding it, treat it as a touch event. src = pointer.Touch default: return } w.callbacks.Event(pointer.Event{ Type: typ, Source: src, Buttons: btns, PointerID: pointer.ID(pointerID), Time: time.Duration(t) * time.Millisecond, Position: f32.Point{X: float32(x), Y: float32(y)}, Scroll: f32.Pt(float32(scrollX), float32(scrollY)), }) } func (w *window) ShowTextInput(show bool) { runOnMain(func(env *C.JNIEnv) { if w.view == 0 { return } if show { callVoidMethod(env, w.view, gioView.showTextInput) } else { callVoidMethod(env, w.view, gioView.hideTextInput) } }) } func javaString(env *C.JNIEnv, str string) C.jstring { if str == "" { return 0 } utf16Chars := utf16.Encode([]rune(str)) return C.gio_jni_NewString(env, (*C.jchar)(unsafe.Pointer(&utf16Chars[0])), C.int(len(utf16Chars))) } func varArgs(args []jvalue) *C.jvalue { if len(args) == 0 { return nil } return (*C.jvalue)(unsafe.Pointer(&args[0])) } func callStaticVoidMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID, args ...jvalue) error { C.gio_jni_CallStaticVoidMethodA(env, cls, method, varArgs(args)) return exception(env) } func callStaticObjectMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID, args ...jvalue) (C.jobject, error) { res := C.gio_jni_CallStaticObjectMethodA(env, cls, method, varArgs(args)) return res, exception(env) } func callVoidMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, args ...jvalue) error { C.gio_jni_CallVoidMethodA(env, obj, method, varArgs(args)) return exception(env) } func callObjectMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, args ...jvalue) (C.jobject, error) { res := C.gio_jni_CallObjectMethodA(env, obj, method, varArgs(args)) return res, exception(env) } // exception returns an error corresponding to the pending // exception, or nil if no exception is pending. The pending // exception is cleared. func exception(env *C.JNIEnv) error { thr := C.gio_jni_ExceptionOccurred(env) if thr == 0 { return nil } C.gio_jni_ExceptionClear(env) cls := getObjectClass(env, C.jobject(thr)) toString := getMethodID(env, cls, "toString", "()Ljava/lang/String;") msg, err := callObjectMethod(env, C.jobject(thr), toString) if err != nil { return err } return errors.New(goString(env, C.jstring(msg))) } func getObjectClass(env *C.JNIEnv, obj C.jobject) C.jclass { if obj == 0 { panic("null object") } cls := C.gio_jni_GetObjectClass(env, C.jobject(obj)) if err := exception(env); err != nil { // GetObjectClass should never fail. panic(err) } return cls } // goString converts the JVM jstring to a Go string. func goString(env *C.JNIEnv, str C.jstring) string { if str == 0 { return "" } strlen := C.gio_jni_GetStringLength(env, C.jstring(str)) chars := C.gio_jni_GetStringChars(env, C.jstring(str)) var utf16Chars []uint16 hdr := (*reflect.SliceHeader)(unsafe.Pointer(&utf16Chars)) hdr.Data = uintptr(unsafe.Pointer(chars)) hdr.Cap = int(strlen) hdr.Len = int(strlen) utf8 := utf16.Decode(utf16Chars) return string(utf8) } func Main() { } func NewWindow(window Callbacks, opts *Options) error { mainWindow.in <- windowAndOptions{window, opts} return <-mainWindow.errs } func (w *window) WriteClipboard(s string) { runOnMain(func(env *C.JNIEnv) { jstr := javaString(env, s) callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard, jvalue(android.appCtx), jvalue(jstr)) }) } func (w *window) ReadClipboard() { runOnMain(func(env *C.JNIEnv) { c, err := callStaticObjectMethod(env, android.gioCls, android.mreadClipboard, jvalue(android.appCtx)) if err != nil { return } content := goString(env, C.jstring(c)) w.callbacks.Event(clipboard.Event{Text: content}) }) } func (w *window) Option(opts *Options) {} func (w *window) SetCursor(name pointer.CursorName) { w.setState(func(state *windowState) { state.cursor = &name }) } // setState adjust the window state on the main thread. func (w *window) setState(f func(state *windowState)) { runOnMain(func(env *C.JNIEnv) { f(&w.newState) if w.view == 0 { // No View attached. The state will be applied at next onCreateView. return } old := w.state state := w.newState applyStateDiff(env, w.view, old, state) w.state = state }) } func applyStateDiff(env *C.JNIEnv, view C.jobject, old, state windowState) { if state.cursor != nil && old.cursor != state.cursor { setCursor(env, view, *state.cursor) } } func setCursor(env *C.JNIEnv, view C.jobject, name pointer.CursorName) { var curID int switch name { default: fallthrough case pointer.CursorDefault: curID = 1000 // TYPE_ARROW case pointer.CursorText: curID = 1008 // TYPE_TEXT case pointer.CursorPointer: curID = 1002 // TYPE_HAND case pointer.CursorCrossHair: curID = 1007 // TYPE_CROSSHAIR case pointer.CursorColResize: curID = 1014 // TYPE_HORIZONTAL_DOUBLE_ARROW case pointer.CursorRowResize: curID = 1015 // TYPE_VERTICAL_DOUBLE_ARROW case pointer.CursorNone: curID = 0 // TYPE_NULL } callVoidMethod(env, view, gioView.setCursor, jvalue(curID)) } // Close the window. Not implemented for Android. func (w *window) Close() {} // runOnMain runs a function on the Java main thread. func runOnMain(f func(env *C.JNIEnv)) { go func() { mainFuncs <- f runInJVM(javaVM(), func(env *C.JNIEnv) { callStaticVoidMethod(env, android.gioCls, android.mwakeupMainThread) }) }() } //export Java_org_gioui_Gio_scheduleMainFuncs func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) { for { select { case f := <-mainFuncs: f(env) default: return } } } func (_ ViewEvent) ImplementsEvent() {}