app/clipboard: implement clipboard for Android

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2020-05-13 17:12:31 +02:00
parent a6dd70b2dc
commit 05dfceb7e7
6 changed files with 260 additions and 32 deletions
+20
View File
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: Unlicense OR MIT
// +build android
// Package clipboard accesses the system clipboard.
package clipboard
import (
"gioui.org/app/internal/window"
)
// Read the content of the clipboard as a string.
func Read() (string, error) {
return window.ReadClipboard()
}
// Write a string to the clipboard.
func Write(s string) error {
return window.WriteClipboard(s)
}
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Unlicense OR MIT
// +build android
package clipboard
import "testing"
func TestClipboard(t *testing.T) {
const want = "Hello, 世界"
if err := Write(want); err != nil {
t.Fatal(err)
}
got, err := Read()
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("read %q from the clipboard, wanted %q", got, want)
}
}
+17 -1
View File
@@ -2,11 +2,13 @@
package org.gioui; package org.gioui;
import android.content.ClipboardManager;
import android.content.ClipData;
import android.content.Context; import android.content.Context;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
public class Gio { public final class Gio {
private final static Object initLock = new Object(); private final static Object initLock = new Object();
private static boolean jniLoaded; private static boolean jniLoaded;
@@ -35,5 +37,19 @@ public class Gio {
} }
} }
private static void writeClipboard(Context ctx, String s) {
ClipboardManager m = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE);
m.setPrimaryClip(ClipData.newPlainText(null, s));
}
private static String readClipboard(Context ctx) {
ClipboardManager m = (ClipboardManager)ctx.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData c = m.getPrimaryClip();
if (c == null || c.getItemCount() < 1) {
return null;
}
return c.getItemAt(0).coerceToText(ctx).toString();
}
static private native void runGoMain(byte[] dataDir, Context context); static private native void runGoMain(byte[] dataDir, Context context);
} }
+29 -1
View File
@@ -48,7 +48,11 @@ jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID) {
return (*env)->CallIntMethod(env, obj, methodID); return (*env)->CallIntMethod(env, obj, methodID);
} }
void gio_jni_CallVoidMethod(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args) { void gio_jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args) {
(*env)->CallStaticVoidMethodA(env, cls, methodID, args);
}
void gio_jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args) {
(*env)->CallVoidMethodA(env, obj, methodID, args); (*env)->CallVoidMethodA(env, obj, methodID, args);
} }
@@ -67,3 +71,27 @@ jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr) {
jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) { jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) {
return (*env)->NewString(env, unicodeChars, len); return (*env)->NewString(env, unicodeChars, len);
} }
jsize gio_jni_GetStringLength(JNIEnv *env, jstring str) {
return (*env)->GetStringLength(env, str);
}
const jchar *gio_jni_GetStringChars(JNIEnv *env, jstring str) {
return (*env)->GetStringChars(env, str, NULL);
}
jthrowable gio_jni_ExceptionOccurred(JNIEnv *env) {
return (*env)->ExceptionOccurred(env);
}
void gio_jni_ExceptionClear(JNIEnv *env) {
(*env)->ExceptionClear(env);
}
jobject gio_jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) {
return (*env)->CallObjectMethodA(env, obj, method, args);
}
jobject gio_jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) {
return (*env)->CallStaticObjectMethodA(env, cls, method, args);
}
+165 -29
View File
@@ -19,6 +19,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
"reflect"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"sync" "sync"
@@ -62,35 +63,54 @@ type jvalue uint64 // The largest JNI type fits in 64 bits.
var dataDirChan = make(chan string, 1) var dataDirChan = make(chan string, 1)
var theJVM *C.JavaVM 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
var appContext C.jobject // The global Android App context.
appCtx C.jobject
// The Gio class reference.
gioCls C.jclass
// Cached Java methods.
writeClipboard C.jmethodID
readClipboard C.jmethodID
}
var views = make(map[C.jlong]*window) var views = make(map[C.jlong]*window)
var mainWindow = newWindowRendezvous() var mainWindow = newWindowRendezvous()
func jniGetMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID { func getMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID {
m := C.CString(method) m := C.CString(method)
defer C.free(unsafe.Pointer(m)) defer C.free(unsafe.Pointer(m))
s := C.CString(sig) s := C.CString(sig)
defer C.free(unsafe.Pointer(s)) defer C.free(unsafe.Pointer(s))
return C.gio_jni_GetMethodID(env, class, m, s) jm := C.gio_jni_GetMethodID(env, class, m, s)
if err := exception(env); err != nil {
panic(err)
}
return jm
} }
func jniGetStaticMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID { func getStaticMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID {
m := C.CString(method) m := C.CString(method)
defer C.free(unsafe.Pointer(m)) defer C.free(unsafe.Pointer(m))
s := C.CString(sig) s := C.CString(sig)
defer C.free(unsafe.Pointer(s)) defer C.free(unsafe.Pointer(s))
return C.gio_jni_GetStaticMethodID(env, class, m, s) jm := C.gio_jni_GetStaticMethodID(env, class, m, s)
if err := exception(env); err != nil {
panic(err)
}
return jm
} }
//export Java_org_gioui_Gio_runGoMain //export Java_org_gioui_Gio_runGoMain
func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass, jdataDir C.jbyteArray, context C.jobject) { func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass, jdataDir C.jbyteArray, context C.jobject) {
if res := C.gio_jni_GetJavaVM(env, &theJVM); res != 0 { initJVM(env, class, context)
panic("gio: GetJavaVM failed")
}
dirBytes := C.gio_jni_GetByteArrayElements(env, jdataDir) dirBytes := C.gio_jni_GetByteArrayElements(env, jdataDir)
if dirBytes == nil { if dirBytes == nil {
panic("runGoMain: GetByteArrayElements failed") panic("runGoMain: GetByteArrayElements failed")
@@ -99,17 +119,37 @@ func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass, jdataDir C.jbyt
dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n) dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n)
dataDirChan <- dataDir dataDirChan <- dataDir
C.gio_jni_ReleaseByteArrayElements(env, jdataDir, dirBytes) C.gio_jni_ReleaseByteArrayElements(env, jdataDir, dirBytes)
appContext = C.gio_jni_NewGlobalRef(env, context)
runMain() runMain()
} }
func initJVM(env *C.JNIEnv, gio C.jclass, ctx C.jobject) {
android.mu.Lock()
defer android.mu.Unlock()
if res := C.gio_jni_GetJavaVM(env, &android.jvm); res != 0 {
panic("gio: GetJavaVM failed")
}
android.writeClipboard = getStaticMethodID(env, gio, "writeClipboard", "(Landroid/content/Context;Ljava/lang/String;)V")
android.readClipboard = getStaticMethodID(env, gio, "readClipboard", "(Landroid/content/Context;)Ljava/lang/String;")
android.appCtx = C.gio_jni_NewGlobalRef(env, ctx)
android.gioCls = C.jclass(C.gio_jni_NewGlobalRef(env, C.jobject(gio)))
}
func JavaVM() uintptr { func JavaVM() uintptr {
return uintptr(unsafe.Pointer(theJVM)) jvm := javaVM()
return uintptr(unsafe.Pointer(jvm))
}
func javaVM() *C.JavaVM {
android.mu.Lock()
defer android.mu.Unlock()
return android.jvm
} }
func AppContext() uintptr { func AppContext() uintptr {
return uintptr(appContext) android.mu.Lock()
defer android.mu.Unlock()
return uintptr(android.appCtx)
} }
func GetDataDir() string { func GetDataDir() string {
@@ -121,13 +161,13 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
view = C.gio_jni_NewGlobalRef(env, view) view = C.gio_jni_NewGlobalRef(env, view)
w := &window{ w := &window{
view: view, view: view,
mgetDensity: jniGetMethodID(env, class, "getDensity", "()I"), mgetDensity: getMethodID(env, class, "getDensity", "()I"),
mgetFontScale: jniGetMethodID(env, class, "getFontScale", "()F"), mgetFontScale: getMethodID(env, class, "getFontScale", "()F"),
mshowTextInput: jniGetMethodID(env, class, "showTextInput", "()V"), mshowTextInput: getMethodID(env, class, "showTextInput", "()V"),
mhideTextInput: jniGetMethodID(env, class, "hideTextInput", "()V"), mhideTextInput: getMethodID(env, class, "hideTextInput", "()V"),
mpostFrameCallback: jniGetMethodID(env, class, "postFrameCallback", "()V"), mpostFrameCallback: getMethodID(env, class, "postFrameCallback", "()V"),
mpostFrameCallbackOnMainThread: jniGetMethodID(env, class, "postFrameCallbackOnMainThread", "()V"), mpostFrameCallbackOnMainThread: getMethodID(env, class, "postFrameCallbackOnMainThread", "()V"),
mRegisterFragment: jniGetMethodID(env, class, "registerFragment", "(Ljava/lang/String;)V"), mRegisterFragment: getMethodID(env, class, "registerFragment", "(Ljava/lang/String;)V"),
} }
wopts := <-mainWindow.out wopts := <-mainWindow.out
w.callbacks = wopts.window w.callbacks = wopts.window
@@ -212,7 +252,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
anim := w.animating anim := w.animating
w.mu.Unlock() w.mu.Unlock()
if anim { if anim {
runInJVM(func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
callVoidMethod(env, w.view, w.mpostFrameCallback) callVoidMethod(env, w.view, w.mpostFrameCallback)
}) })
w.draw(false) w.draw(false)
@@ -306,7 +346,7 @@ func (w *window) SetAnimating(anim bool) {
w.animating = anim w.animating = anim
w.mu.Unlock() w.mu.Unlock()
if anim { if anim {
runInJVM(func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
callVoidMethod(env, w.view, w.mpostFrameCallbackOnMainThread) callVoidMethod(env, w.view, w.mpostFrameCallbackOnMainThread)
}) })
} }
@@ -339,16 +379,19 @@ func (w *window) draw(sync bool) {
type keyMapper func(devId, keyCode C.int32_t) rune type keyMapper func(devId, keyCode C.int32_t) rune
func runInJVM(f func(env *C.JNIEnv)) { func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
if jvm == nil {
panic("nil JVM")
}
runtime.LockOSThread() runtime.LockOSThread()
defer runtime.UnlockOSThread() defer runtime.UnlockOSThread()
var env *C.JNIEnv var env *C.JNIEnv
var detach bool var detach bool
if res := C.gio_jni_GetEnv(theJVM, &env, C.JNI_VERSION_1_6); res != C.JNI_OK { if res := C.gio_jni_GetEnv(jvm, &env, C.JNI_VERSION_1_6); res != C.JNI_OK {
if res != C.JNI_EDETACHED { if res != C.JNI_EDETACHED {
panic(fmt.Errorf("JNI GetEnv failed with error %d", res)) panic(fmt.Errorf("JNI GetEnv failed with error %d", res))
} }
if C.gio_jni_AttachCurrentThread(theJVM, &env, nil) != C.JNI_OK { if C.gio_jni_AttachCurrentThread(jvm, &env, nil) != C.JNI_OK {
panic(errors.New("runInJVM: AttachCurrentThread failed")) panic(errors.New("runInJVM: AttachCurrentThread failed"))
} }
detach = true detach = true
@@ -356,7 +399,7 @@ func runInJVM(f func(env *C.JNIEnv)) {
if detach { if detach {
defer func() { defer func() {
C.gio_jni_DetachCurrentThread(theJVM) C.gio_jni_DetachCurrentThread(jvm)
}() }()
} }
f(env) f(env)
@@ -447,7 +490,7 @@ func (w *window) ShowTextInput(show bool) {
if w.view == 0 { if w.view == 0 {
return return
} }
runInJVM(func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
if show { if show {
callVoidMethod(env, w.view, w.mshowTextInput) callVoidMethod(env, w.view, w.mshowTextInput)
} else { } else {
@@ -465,7 +508,7 @@ func javaString(env *C.JNIEnv, str string) C.jstring {
} }
func (w *window) RegisterFragment(del string) { func (w *window) RegisterFragment(del string) {
runInJVM(func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
jstr := javaString(env, del) jstr := javaString(env, del)
callVoidMethod(env, w.view, w.mRegisterFragment, jvalue(jstr)) callVoidMethod(env, w.view, w.mRegisterFragment, jvalue(jstr))
}) })
@@ -478,8 +521,70 @@ func varArgs(args []jvalue) *C.jvalue {
return (*C.jvalue)(unsafe.Pointer(&args[0])) return (*C.jvalue)(unsafe.Pointer(&args[0]))
} }
func callVoidMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, args ...jvalue) { func callStaticVoidMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID, args ...jvalue) error {
C.gio_jni_CallVoidMethod(env, obj, method, varArgs(args)) C.gio_jni_CallStaticVoidMethodA(env, cls, method, varArgs(args))
return exception(env)
}
func callStaticObjectMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID, args ...jvalue) (C.jobject, error) {
res := C.gio_jni_CallStaticObjectMethodA(env, cls, method, varArgs(args))
return res, exception(env)
}
func callVoidMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, args ...jvalue) error {
C.gio_jni_CallVoidMethodA(env, obj, method, varArgs(args))
return exception(env)
}
func callObjectMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, args ...jvalue) (C.jobject, error) {
res := C.gio_jni_CallObjectMethodA(env, obj, method, varArgs(args))
return res, exception(env)
}
// exception returns an error corresponding to the pending
// exception, or nil if no exception is pending. The pending
// exception is cleared.
func exception(env *C.JNIEnv) error {
thr := C.gio_jni_ExceptionOccurred(env)
if thr == 0 {
return nil
}
C.gio_jni_ExceptionClear(env)
cls := getObjectClass(env, C.jobject(thr))
toString := getMethodID(env, cls, "toString", "()Ljava/lang/String;")
msg, err := callObjectMethod(env, C.jobject(thr), toString)
if err != nil {
return err
}
return errors.New(goString(env, C.jstring(msg)))
}
func getObjectClass(env *C.JNIEnv, obj C.jobject) C.jclass {
if obj == 0 {
panic("null object")
}
cls := C.gio_jni_GetObjectClass(env, C.jobject(obj))
if err := exception(env); err != nil {
// GetObjectClass should never fail.
panic(err)
}
return cls
}
// goString converts the JVM jstring to a Go string.
func goString(env *C.JNIEnv, str C.jstring) string {
if str == 0 {
return ""
}
strlen := C.gio_jni_GetStringLength(env, C.jstring(str))
chars := C.gio_jni_GetStringChars(env, C.jstring(str))
var utf16Chars []uint16
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&utf16Chars))
hdr.Data = uintptr(unsafe.Pointer(chars))
hdr.Cap = int(strlen)
hdr.Len = int(strlen)
utf8 := utf16.Decode(utf16Chars)
return string(utf8)
} }
func Main() { func Main() {
@@ -489,3 +594,34 @@ func NewWindow(window Callbacks, opts *Options) error {
mainWindow.in <- windowAndOptions{window, opts} mainWindow.in <- windowAndOptions{window, opts}
return <-mainWindow.errs return <-mainWindow.errs
} }
func WriteClipboard(s string) error {
var jerr error
jvm := javaVM()
if jvm == nil {
return errors.New("clipboard: the JVM is not yet available")
}
runInJVM(jvm, func(env *C.JNIEnv) {
jstr := javaString(env, s)
jerr = callStaticVoidMethod(env, android.gioCls, android.writeClipboard, jvalue(android.appCtx), jvalue(jstr))
})
return jerr
}
func ReadClipboard() (string, error) {
var clipboard string
var jerr error
jvm := javaVM()
if jvm == nil {
return "", errors.New("clipboard: the JVM is not yet available")
}
runInJVM(jvm, func(env *C.JNIEnv) {
c, err := callStaticObjectMethod(env, android.gioCls, android.readClipboard, jvalue(android.appCtx))
if err != nil {
jerr = err
return
}
clipboard = goString(env, C.jstring(c))
})
return clipboard, jerr
}
+8 -1
View File
@@ -12,8 +12,15 @@ __attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetStaticMethodID(JNIE
__attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig); __attribute__ ((visibility ("hidden"))) jmethodID gio_jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
__attribute__ ((visibility ("hidden"))) jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID); __attribute__ ((visibility ("hidden"))) jfloat gio_jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID);
__attribute__ ((visibility ("hidden"))) jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID); __attribute__ ((visibility ("hidden"))) jint gio_jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID);
__attribute__ ((visibility ("hidden"))) void gio_jni_CallVoidMethod(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args); __attribute__ ((visibility ("hidden"))) void gio_jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
__attribute__ ((visibility ("hidden"))) void gio_jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
__attribute__ ((visibility ("hidden"))) jbyte *gio_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr); __attribute__ ((visibility ("hidden"))) jbyte *gio_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr);
__attribute__ ((visibility ("hidden"))) void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes); __attribute__ ((visibility ("hidden"))) void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes);
__attribute__ ((visibility ("hidden"))) jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr); __attribute__ ((visibility ("hidden"))) jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr);
__attribute__ ((visibility ("hidden"))) jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len); __attribute__ ((visibility ("hidden"))) jstring gio_jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);
__attribute__ ((visibility ("hidden"))) jsize gio_jni_GetStringLength(JNIEnv *env, jstring str);
__attribute__ ((visibility ("hidden"))) const jchar *gio_jni_GetStringChars(JNIEnv *env, jstring str);
__attribute__ ((visibility ("hidden"))) jthrowable gio_jni_ExceptionOccurred(JNIEnv *env);
__attribute__ ((visibility ("hidden"))) void gio_jni_ExceptionClear(JNIEnv *env);
__attribute__ ((visibility ("hidden"))) jobject gio_jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args);
__attribute__ ((visibility ("hidden"))) jobject gio_jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args);