// 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"
	"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
		diffs   []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, "<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.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)))
		}
		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)))
		}
	}
}

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() {}
