Files
gio-patched/app/os_android.go
T
inkeliz 7bcb315ee1 app: add custom scheme support
Now, it's possible to launch one Gio app using a custom URI scheme, such as `gio://some/data`.

This feature is supported on Android, iOS, macOS and Windows, issuing a new transfer.URLEvent,
containing the URL launched. If the program is already open, one transfer.URLEvent will be
sent to the current  app.

Limitations:
On Windows, if the program listen to schemes (compiled with `-schemes`), then just a single
instance of the app can be open. In other words, just a single `myprogram.exe` can
be active.

Security:
Deeplinking have the same level of security of clipboard. Any other software can send such
information and read the content, without any restriction. That should not be used to transfer
sensible data, and can't be fully trusted.

Setup/Compiling:
In order to set the custom scheme, you need to use the new `-schemes` flag in `gogio`, using
as `-schemes gio` will listen to `gio://`.

If you are not using gogio you need to defined some values, which varies for each OS:

macOS/iOS - You need to define the following Properly List:
```
<key>CFBundleURLTypes</key>
<array>
  <dict>
        <key>CFBundleURLSchemes</key>
        <array>
          <string>yourCustomScheme</string>
        </array>
  </dict>
</array>
```

Windows - You need to compiling using -X argument:
```
-ldflags="-X "gioui.org/app.schemesURI=yourCustomScheme" -H=windowsgui"
```

Android - You need to add IntentFilter in GioActivity:
```
<intent-filter>
        <action android:name="android.intent.action.VIEW"></action>
        <category android:name="android.intent.category.DEFAULT"></category>
        <category android:name="android.intent.category.BROWSABLE"></category>
        <data android:scheme="yourCustomScheme"></data>
</intent-filter>
```

That assumes that you still using GioActivity and GioAppDelegate, otherwise more
changes are required.

Events are routed to a new app.Events, which are not linked to a specific window.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-15 22:20:54 +01:00

1511 lines
45 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package app
/*
#cgo CFLAGS: -Werror
#cgo LDFLAGS: -landroid
#include <android/native_window_jni.h>
#include <android/configuration.h>
#include <android/keycodes.h>
#include <android/input.h>
#include <stdlib.h>
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"
"gioui.org/io/transfer"
"image"
"image/color"
"io"
"math"
"os"
"path/filepath"
"runtime"
"runtime/cgo"
"runtime/debug"
"strings"
"sync"
"time"
"unicode/utf16"
"unsafe"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/unit"
)
type window struct {
callbacks *callbacks
loop *eventLoop
view C.jobject
handle cgo.Handle
dpi int
fontScale float32
insets pixelInsets
visible bool
started bool
animating bool
win *C.ANativeWindow
config Config
inputHint key.InputHint
semantic struct {
hoverID input.SemanticID
rootID input.SemanticID
focusID input.SemanticID
diffs []input.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
postFrameCallback C.jmethodID
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
restartInput C.jmethodID
updateSelection C.jmethodID
updateCaret C.jmethodID
}
type pixelInsets struct {
top, bottom, left, right int
}
// AndroidViewEvent is sent whenever the Window's underlying Android view
// changes.
type AndroidViewEvent 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
}
}
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){newAndroidGLESContext, newAndroidVulkanContext}
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
})
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)
// Set XDG_CACHE_HOME to make os.UserCacheDir work.
if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists {
cachePath := filepath.Join(dataDir, "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(dataDir, "config")
os.Setenv("XDG_CONFIG_HOME", cfgPath)
}
// Set HOME to make os.UserHomeDir work.
if _, exists := os.LookupEnv("HOME"); !exists {
os.Setenv("HOME", dataDir)
}
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, "<init>", "(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.postFrameCallback = getMethodID(env, class, "postFrameCallback", "()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")
m.restartInput = getMethodID(env, class, "restartInput", "()V")
m.updateSelection = getMethodID(env, class, "updateSelection", "()V")
m.updateCaret = getMethodID(env, class, "updateCaret", "(FFFFFFFFFF)V")
})
view = C.jni_NewGlobalRef(env, view)
wopts := <-mainWindow.out
var cnf Config
w, ok := windows[wopts.window]
if !ok {
w = &window{
callbacks: wopts.window,
}
w.loop = newEventLoop(w.callbacks, w.wakeup)
w.callbacks.SetDriver(w)
cnf.apply(unit.Metric{}, wopts.options)
windows[wopts.window] = w
} else {
cnf = w.config
}
mainWindow.windows <- struct{}{}
if w.view != 0 {
w.detach(env)
}
w.view = view
w.visible = false
w.handle = cgo.NewHandle(w)
w.loadConfig(env, class)
w.setConfig(env, cnf)
w.SetInputHint(w.inputHint)
w.processEvent(AndroidViewEvent{View: uintptr(view)})
return C.jlong(w.handle)
}
//export Java_org_gioui_GioView_onDestroyView
func Java_org_gioui_GioView_onDestroyView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
w := cgo.Handle(handle).Value().(*window)
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 := cgo.Handle(handle).Value().(*window)
w.started = false
w.visible = false
}
//export Java_org_gioui_GioView_onStartView
func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
w := cgo.Handle(handle).Value().(*window)
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 := cgo.Handle(handle).Value().(*window)
w.win = nil
w.visible = false
}
//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 := cgo.Handle(handle).Value().(*window)
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 := cgo.Handle(view).Value().(*window)
w.loadConfig(env, class)
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 := cgo.Handle(view).Value().(*window)
if !exist {
return
}
w.draw(env, 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 := cgo.Handle(view).Value().(*window)
if w.processEvent(key.Event{Name: key.NameBack}) {
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 := cgo.Handle(view).Value().(*window)
w.config.Focused = focus == C.JNI_TRUE
w.processEvent(ConfigEvent{Config: w.config})
}
//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 := cgo.Handle(view).Value().(*window)
w.insets = pixelInsets{
top: int(top),
bottom: int(bottom),
left: int(left),
right: int(right),
}
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 := cgo.Handle(view).Value().(*window)
semID := w.semIDFor(virtID)
sem, found := w.callbacks.LookupSemantic(semID)
if found {
off := image.Pt(int(screenX), int(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 := cgo.Handle(view).Value().(*window)
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 := cgo.Handle(view).Value().(*window)
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 := cgo.Handle(view).Value().(*window)
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 := cgo.Handle(view).Value().(*window)
if w.semantic.focusID == w.semIDFor(virtID) {
w.semantic.focusID = 0
}
}
//export Java_org_gioui_GioView_onOpenURI
func Java_org_gioui_GioView_onOpenURI(env *C.JNIEnv, class C.jclass, view C.jlong, uri C.jstring) {
evt, err := newURLEvent(goString(env, uri))
if err != nil {
return
}
processGlobalEvent(evt)
}
func (w *window) ProcessEvent(e event.Event) {
w.processEvent(e)
}
func (w *window) processEvent(e event.Event) bool {
if !w.callbacks.ProcessEvent(e) {
return false
}
w.loop.FlushEvents()
return true
}
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode, off image.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&input.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 input.SemanticID) C.jint {
if id == w.semantic.rootID {
return HOST_VIEW_ID
}
return C.jint(id)
}
func (w *window) semIDFor(virtID C.jint) input.SemanticID {
if virtID == HOST_VIEW_ID {
return w.semantic.rootID
}
return input.SemanticID(virtID)
}
func (w *window) detach(env *C.JNIEnv) {
callVoidMethod(env, w.view, gioView.unregister)
w.processEvent(AndroidViewEvent{})
w.handle.Delete()
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.visible = true
w.draw(env, true)
}
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.postFrameCallback)
})
}
}
func (w *window) draw(env *C.JNIEnv, sync bool) {
if !w.visible {
return
}
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.processEvent(ConfigEvent{Config: w.config})
}
if size.X == 0 || size.Y == 0 {
return
}
const inchPrDp = 1.0 / 160
ppdp := float32(w.dpi) * inchPrDp
dppp := unit.Dp(1.0 / ppdp)
insets := Insets{
Top: unit.Dp(w.insets.top) * dppp,
Bottom: unit.Dp(w.insets.bottom) * dppp,
Left: unit.Dp(w.insets.left) * dppp,
Right: unit.Dp(w.insets.right) * dppp,
}
w.processEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Insets: insets,
Metric: unit.Metric{
PxPerDp: ppdp,
PxPerSp: w.fontScale * ppdp,
},
},
Sync: sync,
})
if w.animating {
callVoidMethod(env, w.view, gioView.postFrameCallback)
}
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)))
}
w.semantic.diffs = w.callbacks.AppendSemanticDiffs(w.semantic.diffs[:0])
for _, id := range w.semantic.diffs {
callVoidMethod(env, w.view, gioView.sendA11yChange, jvalue(w.virtualIDFor(id)))
}
}
}
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) (key.Name, bool) {
var n key.Name
switch code {
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.NameReturn
case C.AKEYCODE_CTRL_LEFT, C.AKEYCODE_CTRL_RIGHT:
n = key.NameCtrl
case C.AKEYCODE_SHIFT_LEFT, C.AKEYCODE_SHIFT_RIGHT:
n = key.NameShift
case C.AKEYCODE_ALT_LEFT, C.AKEYCODE_ALT_RIGHT:
n = key.NameAlt
case C.AKEYCODE_META_LEFT, C.AKEYCODE_META_RIGHT:
n = key.NameSuper
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
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, pressed C.jboolean, t C.jlong) {
w := cgo.Handle(handle).Value().(*window)
if pressed == C.JNI_TRUE && keyCode == C.AKEYCODE_DPAD_CENTER {
w.callbacks.ClickFocus()
return
}
if n, ok := convertKeyCode(keyCode); ok {
state := key.Release
if pressed == C.JNI_TRUE {
state = key.Press
}
w.processEvent(key.Event{Name: n, State: state})
}
if pressed == C.JNI_TRUE && r != 0 && r != '\n' { // Checking for "\n" to prevent duplication with key.NameEnter (gio#224).
w.callbacks.EditorInsert(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 := cgo.Handle(handle).Value().(*window)
var kind pointer.Kind
switch action {
case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN:
kind = pointer.Press
case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP:
kind = pointer.Release
case C.AMOTION_EVENT_ACTION_CANCEL:
kind = pointer.Cancel
case C.AMOTION_EVENT_ACTION_MOVE:
kind = pointer.Move
case C.AMOTION_EVENT_ACTION_SCROLL:
kind = 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.processEvent(pointer.Event{
Kind: kind,
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)),
})
}
//export Java_org_gioui_GioView_imeSelectionStart
func Java_org_gioui_GioView_imeSelectionStart(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint {
w := cgo.Handle(handle).Value().(*window)
sel := w.callbacks.EditorState().Selection
start := sel.Start
if sel.End < sel.Start {
start = sel.End
}
return C.jint(start)
}
//export Java_org_gioui_GioView_imeSelectionEnd
func Java_org_gioui_GioView_imeSelectionEnd(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint {
w := cgo.Handle(handle).Value().(*window)
sel := w.callbacks.EditorState().Selection
end := sel.End
if sel.End < sel.Start {
end = sel.Start
}
return C.jint(end)
}
//export Java_org_gioui_GioView_imeComposingStart
func Java_org_gioui_GioView_imeComposingStart(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint {
w := cgo.Handle(handle).Value().(*window)
comp := w.callbacks.EditorState().compose
start := comp.Start
if e := comp.End; e < start {
start = e
}
return C.jint(start)
}
//export Java_org_gioui_GioView_imeComposingEnd
func Java_org_gioui_GioView_imeComposingEnd(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint {
w := cgo.Handle(handle).Value().(*window)
comp := w.callbacks.EditorState().compose
end := comp.End
if s := comp.Start; s > end {
end = s
}
return C.jint(end)
}
//export Java_org_gioui_GioView_imeSnippet
func Java_org_gioui_GioView_imeSnippet(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jstring {
w := cgo.Handle(handle).Value().(*window)
snip := w.callbacks.EditorState().Snippet.Text
return javaString(env, snip)
}
//export Java_org_gioui_GioView_imeSnippetStart
func Java_org_gioui_GioView_imeSnippetStart(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint {
w := cgo.Handle(handle).Value().(*window)
return C.jint(w.callbacks.EditorState().Snippet.Start)
}
//export Java_org_gioui_GioView_imeSetSnippet
func Java_org_gioui_GioView_imeSetSnippet(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) {
w := cgo.Handle(handle).Value().(*window)
if start < 0 {
start = 0
}
if end < start {
end = start
}
r := key.Range{Start: int(start), End: int(end)}
w.callbacks.SetEditorSnippet(r)
}
//export Java_org_gioui_GioView_imeSetSelection
func Java_org_gioui_GioView_imeSetSelection(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) {
w := cgo.Handle(handle).Value().(*window)
r := key.Range{Start: int(start), End: int(end)}
w.callbacks.SetEditorSelection(r)
}
//export Java_org_gioui_GioView_imeSetComposingRegion
func Java_org_gioui_GioView_imeSetComposingRegion(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) {
w := cgo.Handle(handle).Value().(*window)
w.callbacks.SetComposingRegion(key.Range{
Start: int(start),
End: int(end),
})
}
//export Java_org_gioui_GioView_imeReplace
func Java_org_gioui_GioView_imeReplace(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint, jtext C.jstring) {
w := cgo.Handle(handle).Value().(*window)
r := key.Range{Start: int(start), End: int(end)}
text := goString(env, jtext)
w.callbacks.EditorReplace(r, text)
}
//export Java_org_gioui_GioView_imeToRunes
func Java_org_gioui_GioView_imeToRunes(env *C.JNIEnv, class C.jclass, handle C.jlong, chars C.jint) C.jint {
w := cgo.Handle(handle).Value().(*window)
state := w.callbacks.EditorState()
return C.jint(state.RunesIndex(int(chars)))
}
//export Java_org_gioui_GioView_imeToUTF16
func Java_org_gioui_GioView_imeToUTF16(env *C.JNIEnv, class C.jclass, handle C.jlong, runes C.jint) C.jint {
w := cgo.Handle(handle).Value().(*window)
state := w.callbacks.EditorState()
return C.jint(state.UTF16Index(int(runes)))
}
func (w *window) EditorStateChanged(old, new editorState) {
runInJVM(javaVM(), func(env *C.JNIEnv) {
if old.Snippet != new.Snippet {
callVoidMethod(env, w.view, gioView.restartInput)
return
}
if old.Selection.Range != new.Selection.Range {
w.callbacks.SetComposingRegion(key.Range{Start: -1, End: -1})
callVoidMethod(env, w.view, gioView.updateSelection)
}
if old.Selection.Transform != new.Selection.Transform || old.Selection.Caret != new.Selection.Caret {
sel := new.Selection
m00, m01, m02, m10, m11, m12 := sel.Transform.Elems()
f := func(v float32) jvalue {
return jvalue(math.Float32bits(v))
}
c := sel.Caret
callVoidMethod(env, w.view, gioView.updateCaret, f(m00), f(m01), f(m02), f(m10), f(m11), f(m12), f(c.Pos.X), f(c.Pos.Y-c.Ascent), f(c.Pos.Y), f(c.Pos.Y+c.Descent))
}
})
}
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) {
w.inputHint = mode
// Constants defined at https://developer.android.com/reference/android/text/InputType.
const (
TYPE_NULL = 0
TYPE_CLASS_TEXT = 1
TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 32
TYPE_TEXT_VARIATION_URI = 16
TYPE_TEXT_VARIATION_PASSWORD = 128
TYPE_TEXT_FLAG_CAP_SENTENCES = 16384
TYPE_TEXT_FLAG_AUTO_CORRECT = 32768
TYPE_CLASS_NUMBER = 2
TYPE_NUMBER_FLAG_DECIMAL = 8192
TYPE_NUMBER_FLAG_SIGNED = 4096
TYPE_CLASS_PHONE = 3
)
runInJVM(javaVM(), func(env *C.JNIEnv) {
var m jvalue
switch mode {
case key.HintText:
m = TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_AUTO_CORRECT | TYPE_TEXT_FLAG_CAP_SENTENCES
case key.HintNumeric:
m = TYPE_CLASS_NUMBER | TYPE_NUMBER_FLAG_DECIMAL | TYPE_NUMBER_FLAG_SIGNED
case key.HintEmail:
m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_EMAIL_ADDRESS
case key.HintURL:
m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_URI
case key.HintTelephone:
m = TYPE_CLASS_PHONE
case key.HintPassword:
m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD
default:
m = TYPE_CLASS_TEXT
}
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 {
utf16Chars := utf16.Encode([]rune(str))
var ptr *C.jchar
if len(utf16Chars) > 0 {
ptr = (*C.jchar)(unsafe.Pointer(&utf16Chars[0]))
}
return C.jni_NewString(env, ptr, 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))
utf16Chars := unsafe.Slice((*uint16)(unsafe.Pointer(chars)), 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() {
select {}
}
func newWindow(window *callbacks, options []Option) {
mainWindow.in <- windowAndConfig{window, options}
<-mainWindow.windows
}
func (w *window) WriteClipboard(mime string, s []byte) {
runInJVM(javaVM(), func(env *C.JNIEnv) {
jstr := javaString(env, string(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.processEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
})
}
func (w *window) Configure(options []Option) {
cnf := w.config
cnf.apply(unit.Metric{}, options)
runInJVM(javaVM(), func(env *C.JNIEnv) {
w.setConfig(env, cnf)
})
}
func (w *window) setConfig(env *C.JNIEnv, cnf Config) {
prev := w.config
// Decorations are never disabled.
cnf.Decorated = true
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 cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.processEvent(ConfigEvent{Config: w.config})
}
func (w *window) Perform(system.Action) {}
func (w *window) SetCursor(cursor pointer.Cursor) {
runInJVM(javaVM(), func(env *C.JNIEnv) {
setCursor(env, w.view, cursor)
})
}
func (w *window) wakeup() {
runOnMain(func(env *C.JNIEnv) {
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
var androidCursor = [...]uint16{
pointer.CursorDefault: 1000, // TYPE_ARROW
pointer.CursorNone: 0,
pointer.CursorText: 1008, // TYPE_TEXT
pointer.CursorVerticalText: 1009, // TYPE_VERTICAL_TEXT
pointer.CursorPointer: 1002, // TYPE_HAND
pointer.CursorCrosshair: 1007, // TYPE_CROSSHAIR
pointer.CursorAllScroll: 1013, // TYPE_ALL_SCROLL
pointer.CursorColResize: 1014, // TYPE_HORIZONTAL_DOUBLE_ARROW
pointer.CursorRowResize: 1015, // TYPE_VERTICAL_DOUBLE_ARROW
pointer.CursorGrab: 1020, // TYPE_GRAB
pointer.CursorGrabbing: 1021, // TYPE_GRABBING
pointer.CursorNotAllowed: 1012, // TYPE_NO_DROP
pointer.CursorWait: 1004, // TYPE_WAIT
pointer.CursorProgress: 1000, // TYPE_ARROW
pointer.CursorNorthWestResize: 1017, // TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW
pointer.CursorNorthEastResize: 1016, // TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW
pointer.CursorSouthWestResize: 1016, // TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW
pointer.CursorSouthEastResize: 1017, // TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW
pointer.CursorNorthSouthResize: 1015, // TYPE_VERTICAL_DOUBLE_ARROW
pointer.CursorEastWestResize: 1014, // TYPE_HORIZONTAL_DOUBLE_ARROW
pointer.CursorWestResize: 1014, // TYPE_HORIZONTAL_DOUBLE_ARROW
pointer.CursorEastResize: 1014, // TYPE_HORIZONTAL_DOUBLE_ARROW
pointer.CursorNorthResize: 1015, // TYPE_VERTICAL_DOUBLE_ARROW
pointer.CursorSouthResize: 1015, // TYPE_VERTICAL_DOUBLE_ARROW
pointer.CursorNorthEastSouthWestResize: 1016, // TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW
pointer.CursorNorthWestSouthEastResize: 1017, // TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW
}
func setCursor(env *C.JNIEnv, view C.jobject, cursor pointer.Cursor) {
curID := androidCursor[cursor]
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)),
)
}
// 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 (AndroidViewEvent) implementsViewEvent() {}
func (AndroidViewEvent) ImplementsEvent() {}
func (a AndroidViewEvent) Valid() bool {
return a != (AndroidViewEvent{})
}