// SPDX-License-Identifier: Unlicense OR MIT package app /* #cgo CFLAGS: -Werror #cgo LDFLAGS: -landroid #include #include #include #include #include static jint jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version) { return (*vm)->GetEnv(vm, (void **)env, version); } static jint jni_GetJavaVM(JNIEnv *env, JavaVM **jvm) { return (*env)->GetJavaVM(env, jvm); } static jint jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) { return (*vm)->AttachCurrentThread(vm, p_env, thr_args); } static jint jni_DetachCurrentThread(JavaVM *vm) { return (*vm)->DetachCurrentThread(vm); } static jobject jni_NewGlobalRef(JNIEnv *env, jobject obj) { return (*env)->NewGlobalRef(env, obj); } static void jni_DeleteGlobalRef(JNIEnv *env, jobject obj) { (*env)->DeleteGlobalRef(env, obj); } static jclass jni_GetObjectClass(JNIEnv *env, jobject obj) { return (*env)->GetObjectClass(env, obj); } static jmethodID jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { return (*env)->GetMethodID(env, clazz, name, sig); } static jmethodID jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { return (*env)->GetStaticMethodID(env, clazz, name, sig); } static jfloat jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID) { return (*env)->CallFloatMethod(env, obj, methodID); } static jint jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID) { return (*env)->CallIntMethod(env, obj, methodID); } static void jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args) { (*env)->CallStaticVoidMethodA(env, cls, methodID, args); } static void jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args) { (*env)->CallVoidMethodA(env, obj, methodID, args); } static jboolean jni_CallBooleanMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args) { return (*env)->CallBooleanMethodA(env, obj, methodID, args); } static jbyte *jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr) { return (*env)->GetByteArrayElements(env, arr, NULL); } static void jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes) { (*env)->ReleaseByteArrayElements(env, arr, bytes, JNI_ABORT); } static jsize jni_GetArrayLength(JNIEnv *env, jbyteArray arr) { return (*env)->GetArrayLength(env, arr); } static jstring jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) { return (*env)->NewString(env, unicodeChars, len); } static jsize jni_GetStringLength(JNIEnv *env, jstring str) { return (*env)->GetStringLength(env, str); } static const jchar *jni_GetStringChars(JNIEnv *env, jstring str) { return (*env)->GetStringChars(env, str, NULL); } static jthrowable jni_ExceptionOccurred(JNIEnv *env) { return (*env)->ExceptionOccurred(env); } static void jni_ExceptionClear(JNIEnv *env) { (*env)->ExceptionClear(env); } static jobject jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { return (*env)->CallObjectMethodA(env, obj, method, args); } static jobject jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) { return (*env)->CallStaticObjectMethodA(env, cls, method, args); } static jclass jni_FindClass(JNIEnv *env, char *name) { return (*env)->FindClass(env, name); } static jobject jni_NewObjectA(JNIEnv *env, jclass cls, jmethodID cons, jvalue *args) { return (*env)->NewObjectA(env, cls, cons, args); } */ import "C" import ( "errors" "fmt" "image" "image/color" "os" "path/filepath" "reflect" "runtime" "runtime/debug" "sync" "time" "unicode/utf16" "unsafe" "gioui.org/internal/f32color" "gioui.org/f32" "gioui.org/io/clipboard" "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/router" "gioui.org/io/semantic" "gioui.org/io/system" "gioui.org/unit" ) type window struct { callbacks *callbacks view C.jobject dpi int fontScale float32 insets system.Insets stage system.Stage started bool animating bool win *C.ANativeWindow config Config semantic struct { hoverID router.SemanticID rootID router.SemanticID focusID router.SemanticID } } // 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 setInputHint C.jmethodID postInvalidate C.jmethodID // requests draw, called from non-UI thread invalidate C.jmethodID // requests draw, called from UI thread setCursor C.jmethodID setOrientation C.jmethodID setNavigationColor C.jmethodID setStatusColor C.jmethodID setFullscreen C.jmethodID unregister C.jmethodID sendA11yEvent C.jmethodID sendA11yChange C.jmethodID isA11yActive 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 // android.view.accessibility.AccessibilityNodeInfo class. accessibilityNodeInfo struct { cls C.jclass // addChild(View, int) addChild C.jmethodID // setBoundsInScreen(Rect) setBoundsInScreen C.jmethodID // setText(CharSequence) setText C.jmethodID // setContentDescription(CharSequence) setContentDescription C.jmethodID // setParent(View, int) setParent C.jmethodID // addAction(int) addAction C.jmethodID // setClassName(CharSequence) setClassName C.jmethodID // setCheckable(boolean) setCheckable C.jmethodID // setSelected(boolean) setSelected C.jmethodID // setChecked(boolean) setChecked C.jmethodID // setEnabled(boolean) setEnabled C.jmethodID // setAccessibilityFocused(boolean) setAccessibilityFocused C.jmethodID } // android.graphics.Rect class. rect struct { cls C.jclass // (int, int, int, int) constructor. cons C.jmethodID } strings struct { // "android.view.View" androidViewView C.jstring // "android.widget.Button" androidWidgetButton C.jstring // "android.widget.CheckBox" androidWidgetCheckBox C.jstring // "android.widget.EditText" androidWidgetEditText C.jstring // "android.widget.RadioButton" androidWidgetRadioButton C.jstring // "android.widget.Switch" androidWidgetSwitch C.jstring } } // view maps from GioView JNI refenreces to windows. var views = make(map[C.jlong]*window) var windows = make(map[*callbacks]*window) var mainWindow = newWindowRendezvous() var mainFuncs = make(chan func(env *C.JNIEnv), 1) var ( dataDirOnce sync.Once dataPath string ) var ( newAndroidVulkanContext func(w *window) (context, error) newAndroidGLESContext func(w *window) (context, error) ) // AccessibilityNodeProvider.HOST_VIEW_ID. const HOST_VIEW_ID = -1 const ( // AccessibilityEvent constants. TYPE_VIEW_HOVER_ENTER = 128 TYPE_VIEW_HOVER_EXIT = 256 ) const ( // AccessibilityNodeInfo constants. ACTION_ACCESSIBILITY_FOCUS = 64 ACTION_CLEAR_ACCESSIBILITY_FOCUS = 128 ACTION_CLICK = 16 ) func (w *window) NewContext() (context, error) { funcs := []func(w *window) (context, error){newAndroidVulkanContext, newAndroidGLESContext} var firstErr error for _, f := range funcs { if f == nil { continue } c, err := f(w) if err != nil { if firstErr == nil { firstErr = err } continue } return c, nil } if firstErr != nil { return nil, firstErr } return nil, errors.New("x11: no available GPU backends") } func dataDir() (string, error) { dataDirOnce.Do(func() { dataPath = <-dataDirChan // Set XDG_CACHE_HOME to make os.UserCacheDir work. if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists { cachePath := filepath.Join(dataPath, "cache") os.Setenv("XDG_CACHE_HOME", cachePath) } // Set XDG_CONFIG_HOME to make os.UserConfigDir work. if _, exists := os.LookupEnv("XDG_CONFIG_HOME"); !exists { cfgPath := filepath.Join(dataPath, "config") os.Setenv("XDG_CONFIG_HOME", cfgPath) } // Set HOME to make os.UserHomeDir work. if _, exists := os.LookupEnv("HOME"); !exists { os.Setenv("HOME", dataPath) } }) return dataPath, nil } 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.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.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.jni_GetByteArrayElements(env, jdataDir) if dirBytes == nil { panic("runGoMain: GetByteArrayElements failed") } n := C.jni_GetArrayLength(env, jdataDir) dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n) dataDirChan <- dataDir C.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.jni_GetJavaVM(env, &android.jvm); res != 0 { panic("gio: GetJavaVM failed") } android.appCtx = C.jni_NewGlobalRef(env, ctx) android.gioCls = C.jclass(C.jni_NewGlobalRef(env, C.jobject(gio))) cls := findClass(env, "android/view/accessibility/AccessibilityNodeInfo") android.accessibilityNodeInfo.cls = C.jclass(C.jni_NewGlobalRef(env, C.jobject(cls))) android.accessibilityNodeInfo.addChild = getMethodID(env, cls, "addChild", "(Landroid/view/View;I)V") android.accessibilityNodeInfo.setBoundsInScreen = getMethodID(env, cls, "setBoundsInScreen", "(Landroid/graphics/Rect;)V") android.accessibilityNodeInfo.setText = getMethodID(env, cls, "setText", "(Ljava/lang/CharSequence;)V") android.accessibilityNodeInfo.setContentDescription = getMethodID(env, cls, "setContentDescription", "(Ljava/lang/CharSequence;)V") android.accessibilityNodeInfo.setParent = getMethodID(env, cls, "setParent", "(Landroid/view/View;I)V") android.accessibilityNodeInfo.addAction = getMethodID(env, cls, "addAction", "(I)V") android.accessibilityNodeInfo.setClassName = getMethodID(env, cls, "setClassName", "(Ljava/lang/CharSequence;)V") android.accessibilityNodeInfo.setCheckable = getMethodID(env, cls, "setCheckable", "(Z)V") android.accessibilityNodeInfo.setSelected = getMethodID(env, cls, "setSelected", "(Z)V") android.accessibilityNodeInfo.setChecked = getMethodID(env, cls, "setChecked", "(Z)V") android.accessibilityNodeInfo.setEnabled = getMethodID(env, cls, "setEnabled", "(Z)V") android.accessibilityNodeInfo.setAccessibilityFocused = getMethodID(env, cls, "setAccessibilityFocused", "(Z)V") cls = findClass(env, "android/graphics/Rect") android.rect.cls = C.jclass(C.jni_NewGlobalRef(env, C.jobject(cls))) android.rect.cons = getMethodID(env, cls, "", "(IIII)V") 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") intern := func(s string) C.jstring { ref := C.jni_NewGlobalRef(env, C.jobject(javaString(env, s))) return C.jstring(ref) } android.strings.androidViewView = intern("android.view.View") android.strings.androidWidgetButton = intern("android.widget.Button") android.strings.androidWidgetCheckBox = intern("android.widget.CheckBox") android.strings.androidWidgetEditText = intern("android.widget.EditText") android.strings.androidWidgetRadioButton = intern("android.widget.RadioButton") android.strings.androidWidgetSwitch = intern("android.widget.Switch") } // JavaVM returns the global JNI JavaVM. func JavaVM() uintptr { jvm := javaVM() return uintptr(unsafe.Pointer(jvm)) } func javaVM() *C.JavaVM { android.mu.Lock() defer android.mu.Unlock() return android.jvm } // AppContext returns the global Application context as a JNI jobject. func AppContext() uintptr { android.mu.Lock() defer android.mu.Unlock() return uintptr(android.appCtx) } //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.setInputHint = getMethodID(env, class, "setInputHint", "(I)V") m.postInvalidate = getMethodID(env, class, "postInvalidate", "()V") m.invalidate = getMethodID(env, class, "invalidate", "()V") m.setCursor = getMethodID(env, class, "setCursor", "(I)V") m.setOrientation = getMethodID(env, class, "setOrientation", "(II)V") m.setNavigationColor = getMethodID(env, class, "setNavigationColor", "(II)V") m.setStatusColor = getMethodID(env, class, "setStatusColor", "(II)V") m.setFullscreen = getMethodID(env, class, "setFullscreen", "(Z)V") m.unregister = getMethodID(env, class, "unregister", "()V") m.sendA11yEvent = getMethodID(env, class, "sendA11yEvent", "(II)V") m.sendA11yChange = getMethodID(env, class, "sendA11yChange", "(I)V") m.isA11yActive = getMethodID(env, class, "isA11yActive", "()Z") }) view = C.jni_NewGlobalRef(env, view) wopts := <-mainWindow.out w, ok := windows[wopts.window] if !ok { w = &window{ callbacks: wopts.window, } windows[wopts.window] = w } if w.view != 0 { w.detach(env) } w.view = view w.callbacks.SetDriver(w) handle := C.jlong(view) views[handle] = w w.loadConfig(env, class) w.Configure(wopts.options) 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.detach(env) } //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.win != nil { w.setVisible(env) } } //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.win = nil 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.win = C.ANativeWindow_fromSurface(env, surf) if w.started { w.setVisible(env) } } //export Java_org_gioui_GioView_onLowMemory func Java_org_gioui_GioView_onLowMemory(env *C.JNIEnv, class C.jclass) { 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(env, true) } } //export Java_org_gioui_GioView_onFrameCallback func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view C.jlong) { w, exist := views[view] if !exist { return } if w.stage < system.StageRunning { return } if w.animating { w.draw(env, false) // Schedule the next draw immediately after this one. Since onFrameCallback runs // on the UI thread, View.invalidate can be used here instead of postInvalidate. callVoidMethod(env, w.view, gioView.invalidate) } } //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(env, true) } } //export Java_org_gioui_GioView_initializeAccessibilityNodeInfo func Java_org_gioui_GioView_initializeAccessibilityNodeInfo(env *C.JNIEnv, class C.jclass, view C.jlong, virtID, screenX, screenY C.jint, info C.jobject) C.jobject { w := views[view] semID := w.semIDFor(virtID) sem, found := w.callbacks.LookupSemantic(semID) if found { off := f32.Pt(float32(screenX), float32(screenY)) if err := w.initAccessibilityNodeInfo(env, sem, off, info); err != nil { panic(err) } } return info } //export Java_org_gioui_GioView_onTouchExploration func Java_org_gioui_GioView_onTouchExploration(env *C.JNIEnv, class C.jclass, view C.jlong, x, y C.jfloat) { w := views[view] semID, _ := w.callbacks.SemanticAt(f32.Pt(float32(x), float32(y))) if w.semantic.hoverID == semID { return } // Android expects ENTER before EXIT. if semID != 0 { callVoidMethod(env, w.view, gioView.sendA11yEvent, TYPE_VIEW_HOVER_ENTER, jvalue(w.virtualIDFor(semID))) } if prevID := w.semantic.hoverID; prevID != 0 { callVoidMethod(env, w.view, gioView.sendA11yEvent, TYPE_VIEW_HOVER_EXIT, jvalue(w.virtualIDFor(prevID))) } w.semantic.hoverID = semID } //export Java_org_gioui_GioView_onExitTouchExploration func Java_org_gioui_GioView_onExitTouchExploration(env *C.JNIEnv, class C.jclass, view C.jlong) { w := views[view] if w.semantic.hoverID != 0 { callVoidMethod(env, w.view, gioView.sendA11yEvent, TYPE_VIEW_HOVER_EXIT, jvalue(w.virtualIDFor(w.semantic.hoverID))) w.semantic.hoverID = 0 } } //export Java_org_gioui_GioView_onA11yFocus func Java_org_gioui_GioView_onA11yFocus(env *C.JNIEnv, class C.jclass, view C.jlong, virtID C.jint) { w := views[view] if semID := w.semIDFor(virtID); semID != w.semantic.focusID { w.semantic.focusID = semID // Android needs invalidate to refresh the TalkBack focus indicator. callVoidMethod(env, w.view, gioView.invalidate) } } //export Java_org_gioui_GioView_onClearA11yFocus func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view C.jlong, virtID C.jint) { w := views[view] if w.semantic.focusID == w.semIDFor(virtID) { w.semantic.focusID = 0 } } func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNode, off f32.Point, info C.jobject) error { for _, ch := range sem.Children { err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID))) if err != nil { return err } } if sem.ParentID != 0 { if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setParent, jvalue(w.view), jvalue(w.virtualIDFor(sem.ParentID))); err != nil { return err } b := sem.Desc.Bounds.Add(off) rect, err := newObject(env, android.rect.cls, android.rect.cons, jvalue(b.Min.X), jvalue(b.Min.Y), jvalue(b.Max.X), jvalue(b.Max.Y), ) if err != nil { return err } if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setBoundsInScreen, jvalue(rect)); err != nil { return err } } d := sem.Desc if l := d.Label; l != "" { jlbl := javaString(env, l) if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setText, jvalue(jlbl)); err != nil { return err } } if d.Description != "" { jd := javaString(env, d.Description) if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setContentDescription, jvalue(jd)); err != nil { return err } } addAction := func(act C.jint) { if err := callVoidMethod(env, info, android.accessibilityNodeInfo.addAction, jvalue(act)); err != nil { panic(err) } } if d.Gestures&router.ClickGesture != 0 { addAction(ACTION_CLICK) } clsName := android.strings.androidViewView selectMethod := android.accessibilityNodeInfo.setChecked checkable := false switch d.Class { case semantic.Button: clsName = android.strings.androidWidgetButton case semantic.CheckBox: checkable = true clsName = android.strings.androidWidgetCheckBox case semantic.Editor: clsName = android.strings.androidWidgetEditText case semantic.RadioButton: selectMethod = android.accessibilityNodeInfo.setSelected clsName = android.strings.androidWidgetRadioButton case semantic.Switch: checkable = true clsName = android.strings.androidWidgetSwitch } if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setClassName, jvalue(clsName)); err != nil { panic(err) } if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setCheckable, jvalue(javaBool(checkable))); err != nil { panic(err) } if err := callVoidMethod(env, info, selectMethod, jvalue(javaBool(d.Selected))); err != nil { panic(err) } if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setEnabled, jvalue(javaBool(!d.Disabled))); err != nil { panic(err) } isFocus := w.semantic.focusID == sem.ID if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setAccessibilityFocused, jvalue(javaBool(isFocus))); err != nil { panic(err) } if isFocus { addAction(ACTION_CLEAR_ACCESSIBILITY_FOCUS) } else { addAction(ACTION_ACCESSIBILITY_FOCUS) } return nil } func (w *window) virtualIDFor(id router.SemanticID) C.jint { // TODO: Android virtual IDs are 32-bit Java integers, but childID is a int64. if id == w.semantic.rootID { return HOST_VIEW_ID } return C.jint(id) } func (w *window) semIDFor(virtID C.jint) router.SemanticID { if virtID == HOST_VIEW_ID { return w.semantic.rootID } return router.SemanticID(virtID) } func (w *window) detach(env *C.JNIEnv) { callVoidMethod(env, w.view, gioView.unregister) w.callbacks.Event(ViewEvent{}) w.callbacks.SetDriver(nil) delete(views, C.jlong(w.view)) C.jni_DeleteGlobalRef(env, w.view) w.view = 0 } func (w *window) setVisible(env *C.JNIEnv) { width, height := C.ANativeWindow_getWidth(w.win), C.ANativeWindow_getHeight(w.win) if width == 0 || height == 0 { return } w.setStage(system.StageRunning) w.draw(env, 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) setVisual(visID int) error { if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 { return errors.New("ANativeWindow_setBuffersGeometry failed") } return nil } func (w *window) nativeWindow() (*C.ANativeWindow, int, int) { width, height := C.ANativeWindow_getWidth(w.win), C.ANativeWindow_getHeight(w.win) return w.win, int(width), int(height) } func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) { dpi := int(C.jni_CallIntMethod(env, w.view, gioView.getDensity)) w.fontScale = float32(C.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.animating = anim if anim { runInJVM(javaVM(), func(env *C.JNIEnv) { callVoidMethod(env, w.view, gioView.postInvalidate) }) } } func (w *window) draw(env *C.JNIEnv, sync bool) { size := image.Pt(int(C.ANativeWindow_getWidth(w.win)), int(C.ANativeWindow_getHeight(w.win))) if size != w.config.Size { w.config.Size = size w.callbacks.Event(ConfigEvent{Config: w.config}) } if size.X == 0 || size.Y == 0 { return } const inchPrDp = 1.0 / 160 ppdp := float32(w.dpi) * inchPrDp w.callbacks.Event(frameEvent{ FrameEvent: system.FrameEvent{ Now: time.Now(), Size: w.config.Size, Insets: w.insets, Metric: unit.Metric{ PxPerDp: ppdp, PxPerSp: w.fontScale * ppdp, }, }, Sync: sync, }) a11yActive, err := callBooleanMethod(env, w.view, gioView.isA11yActive) if err != nil { panic(err) } if a11yActive { if newR, oldR := w.callbacks.SemanticRoot(), w.semantic.rootID; newR != oldR { // Remap focus and hover. if oldR == w.semantic.hoverID { w.semantic.hoverID = newR } if oldR == w.semantic.focusID { w.semantic.focusID = newR } w.semantic.rootID = newR callVoidMethod(env, w.view, gioView.sendA11yChange, jvalue(w.virtualIDFor(newR))) } diffs := w.callbacks.RequestSemanticDiffs() for { id := <-diffs if id == 0 { break } callVoidMethod(env, w.view, gioView.sendA11yChange, jvalue(w.virtualIDFor(id))) } } } 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.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.jni_AttachCurrentThread(jvm, &env, nil) != C.JNI_OK { panic(errors.New("runInJVM: AttachCurrentThread failed")) } defer C.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 && r != '\n' { // Checking for "\n" to prevent duplication with key.NameEnter (gio#224). 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_STYLUS: 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) { runInJVM(javaVM(), func(env *C.JNIEnv) { if show { callVoidMethod(env, w.view, gioView.showTextInput) } else { callVoidMethod(env, w.view, gioView.hideTextInput) } }) } func (w *window) SetInputHint(mode key.InputHint) { // Constants defined at https://developer.android.com/reference/android/text/InputType. const ( TYPE_NULL = 0 TYPE_CLASS_NUMBER = 2 TYPE_NUMBER_FLAG_DECIMAL = 8192 TYPE_NUMBER_FLAG_SIGNED = 4096 TYPE_TEXT_FLAG_NO_SUGGESTIONS = 524288 TYPE_TEXT_VARIATION_VISIBLE_PASSWORD = 144 ) runInJVM(javaVM(), func(env *C.JNIEnv) { var m jvalue switch mode { case key.HintNumeric: m = TYPE_CLASS_NUMBER | TYPE_NUMBER_FLAG_DECIMAL | TYPE_NUMBER_FLAG_SIGNED default: // TYPE_NULL, since TYPE_CLASS_TEXT isn't currently supported. m = TYPE_NULL } // The TYPE_TEXT_FLAG_NO_SUGGESTIONS and TYPE_TEXT_VARIATION_VISIBLE_PASSWORD are used to fix the // Samsung keyboard compatibility, forcing to disable the suggests/auto-complete. gio#116. m = m | TYPE_TEXT_FLAG_NO_SUGGESTIONS | TYPE_TEXT_VARIATION_VISIBLE_PASSWORD callVoidMethod(env, w.view, gioView.setInputHint, m) }) } func javaBool(b bool) C.jboolean { if b { return C.JNI_TRUE } else { return C.JNI_FALSE } } func javaString(env *C.JNIEnv, str string) C.jstring { if str == "" { return 0 } utf16Chars := utf16.Encode([]rune(str)) return C.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.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.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.jni_CallVoidMethodA(env, obj, method, varArgs(args)) return exception(env) } func callBooleanMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, args ...jvalue) (bool, error) { res := C.jni_CallBooleanMethodA(env, obj, method, varArgs(args)) return res == C.JNI_TRUE, exception(env) } func callObjectMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, args ...jvalue) (C.jobject, error) { res := C.jni_CallObjectMethodA(env, obj, method, varArgs(args)) return res, exception(env) } func newObject(env *C.JNIEnv, cls C.jclass, method C.jmethodID, args ...jvalue) (C.jobject, error) { res := C.jni_NewObjectA(env, cls, 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.jni_ExceptionOccurred(env) if thr == 0 { return nil } C.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.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.jni_GetStringLength(env, C.jstring(str)) chars := C.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 findClass(env *C.JNIEnv, name string) C.jclass { cn := C.CString(name) defer C.free(unsafe.Pointer(cn)) return C.jni_FindClass(env, cn) } func osMain() { } func newWindow(window *callbacks, options []Option) error { mainWindow.in <- windowAndConfig{window, options} return <-mainWindow.errs } func (w *window) WriteClipboard(s string) { runInJVM(javaVM(), func(env *C.JNIEnv) { jstr := javaString(env, s) callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard, jvalue(android.appCtx), jvalue(jstr)) }) } func (w *window) ReadClipboard() { runInJVM(javaVM(), 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) Configure(options []Option) { runInJVM(javaVM(), func(env *C.JNIEnv) { prev := w.config cnf := w.config cnf.apply(unit.Metric{}, options) if prev.Orientation != cnf.Orientation { w.config.Orientation = cnf.Orientation setOrientation(env, w.view, cnf.Orientation) } if prev.NavigationColor != cnf.NavigationColor { w.config.NavigationColor = cnf.NavigationColor setNavigationColor(env, w.view, cnf.NavigationColor) } if prev.StatusColor != cnf.StatusColor { w.config.StatusColor = cnf.StatusColor setStatusColor(env, w.view, cnf.StatusColor) } if prev.Mode != cnf.Mode { switch cnf.Mode { case Fullscreen: callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE) w.config.Mode = Fullscreen case Windowed: callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE) w.config.Mode = Windowed } } if w.config != prev { w.callbacks.Event(ConfigEvent{Config: w.config}) } }) } func (w *window) Raise() {} func (w *window) SetCursor(name pointer.CursorName) { runInJVM(javaVM(), func(env *C.JNIEnv) { setCursor(env, w.view, name) }) } func (w *window) Wakeup() { runOnMain(func(env *C.JNIEnv) { w.callbacks.Event(wakeupEvent{}) }) } 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)) } func setOrientation(env *C.JNIEnv, view C.jobject, mode Orientation) { var ( id int idFallback int // Used only for SDK 17 or older. ) // Constants defined at https://developer.android.com/reference/android/content/pm/ActivityInfo. switch mode { case AnyOrientation: id, idFallback = 2, 2 // SCREEN_ORIENTATION_USER case LandscapeOrientation: id, idFallback = 11, 0 // SCREEN_ORIENTATION_USER_LANDSCAPE (or SCREEN_ORIENTATION_LANDSCAPE) case PortraitOrientation: id, idFallback = 12, 1 // SCREEN_ORIENTATION_USER_PORTRAIT (or SCREEN_ORIENTATION_PORTRAIT) } callVoidMethod(env, view, gioView.setOrientation, jvalue(id), jvalue(idFallback)) } func setStatusColor(env *C.JNIEnv, view C.jobject, color color.NRGBA) { callVoidMethod(env, view, gioView.setStatusColor, jvalue(uint32(color.A)<<24|uint32(color.R)<<16|uint32(color.G)<<8|uint32(color.B)), jvalue(int(f32color.LinearFromSRGB(color).Luminance()*255)), ) } func setNavigationColor(env *C.JNIEnv, view C.jobject, color color.NRGBA) { callVoidMethod(env, view, gioView.setNavigationColor, jvalue(uint32(color.A)<<24|uint32(color.R)<<16|uint32(color.G)<<8|uint32(color.B)), jvalue(int(f32color.LinearFromSRGB(color).Luminance()*255)), ) } // Close the window. Not implemented for Android. func (w *window) Close() {} // Maximize maximizes the window. Not implemented for Android. func (w *window) Maximize() {} // Center the window. Not implemented for Android. func (w *window) Center() {} // 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() {}