Greg Pomerantz: 1 app: add RegisterFragment method on *Window for Android 7 files changed, 140 insertions(+), 15 deletions(-)
Ok, are you looking for me to send this as a new kind of FragmentEvent (via app.Window.in) or in a new channel? If we use Window.in, I'll define a FragmentEvent struct that includes the class name (string) and a chan error to return errors (or to be closed on success). Fragment.getContext() should always return the associated Context regardless of where the constructor is called, so I'm not sure it is
SGTM, except that the event name will be unexported ("fragmentEvent")
necessary to move this to the UI thread.
BTW, please send further patches to the ~eliasnaur/gio-patches mailing list, which I recently added to separate discussion from patches.
Gregory Pomerantz <gmp.gio@wow.st>FYI just sent this over to the patches list. A proof-of-concept Bluetooth LE fragment that can scan and connect to devices is also running.
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~eliasnaur/gio/patches/9109/mbox | git am -3Learn more about email & git
RegisterFragment creates an instance of a Java class and attempts to register it as a Fragment in the window's Context. Signed-off-by: Greg Pomerantz <gmp.gio@wow.st> --- app/app.go | 7 ---- app/app_android.go | 29 ++++++++++++++++ app/internal/window/GioView.java | 70 ++++++++++++++++++++++++++++++++++++++- app/internal/window/handle.go | 7 ---- app/internal/window/os_android.c | 13 ++++++++ app/internal/window/os_android.go | 26 +++++++++++++++ app/internal/window/os_android.h | 3 ++ 7 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 app/app_android.go delete mode 100644 app/internal/window/handle.go diff --git a/app/app.go b/app/app.go index 9a18ec4..71bc57b 100644 --- a/app/app.go +++ b/app/app.go @@ -9,13 +9,6 @@ import ( "gioui.org/app/internal/window" ) -type Handle window.Handle - -// PlatformHandle returns the platform specific Handle. -func PlatformHandle() *Handle { - return (*Handle)(window.PlatformHandle) -} - // extraArgs contains extra arguments to append to // os.Args. The arguments are separated with |. // Useful for running programs on mobiles where the diff --git a/app/app_android.go b/app/app_android.go new file mode 100644 index 0000000..91fb4f1 --- /dev/null +++ b/app/app_android.go @@ -0,0 +1,29 @@ +package app + +import ( + "errors" + + "gioui.org/app/internal/window" +) + +type Handle window.Handle + +// PlatformHandle returns the Android platform-specific Handle. +func PlatformHandle() *Handle { + return (*Handle)(window.PlatformHandle) +} + +// RegisterFragment constructs a Java instance of the specified class +// and attempts to register it as a Fragment in the Context in which +// the View was created. +// +// NOTE: This method must not be called from the Gio application's main +// event loop, as that would block the View's UI thread and result in a +// deadlock. +func (w *Window) RegisterFragment(del string) error { + if w.driver == nil { + return errors.New("RegisterFragment: no window driver found") + } + d := w.driver.(window.AndroidDriver) + return d.RegisterFragment(del) +} diff --git a/app/internal/window/GioView.java b/app/internal/window/GioView.java index 82bc36f..d601269 100644 --- a/app/internal/window/GioView.java +++ b/app/internal/window/GioView.java @@ -2,12 +2,27 @@ package org.gioui; +import java.lang.Class; +import java.lang.ClassLoader; +import java.lang.reflect.Method; +import java.lang.IllegalAccessException; +import java.lang.InstantiationException; +import java.lang.ExceptionInInitializerError; +import java.lang.NoSuchMethodException; +import java.lang.NullPointerException; +import java.lang.SecurityException; +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; import android.content.Context; import android.graphics.Rect; import android.os.Build; import android.os.Handler; -import android.util.AttributeSet; import android.text.Editable; +import android.util.AttributeSet; import android.view.Choreographer; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -207,6 +222,59 @@ public class GioView extends SurfaceView implements Choreographer.FrameCallback return onBack(nhandle); } + public String registerFragment(String del) { + final Class cls; + final Fragment frag; + try { + cls = getContext().getClassLoader().loadClass(del); + frag = (Fragment)cls.newInstance(); + } + catch (ClassNotFoundException ignored) {
If it's all the same to you, I prefer to have the catch statement on the same line as the ending }: try { } catch (...) { }
+ return "RegisterFragment: fragment class not found"; + } + catch (IllegalAccessException | InstantiationException | ExceptionInInitializerError ignored) { + return "RegisterFragment: cannot instantiate delegate object"; + } + catch (SecurityException ignored) { + return "RegisterFragment: security exception"; + } + catch (ClassCastException ignored) { + return "RegisterFragment: provided class does not extend Fragment";
Collapse all these exception types to one error that includes the detail from exception.getMessage().
+ } + + final BlockingQueue<String> q = new LinkedBlockingQueue<String>(); + + handler.post(new Runnable() { + public void run() { + final Context ctx = getContext(); + if (ctx == null) { + q.add("RegisterFragment: GioView has null context"); + return; + } + final FragmentManager fm; + try { + final Method mth = ctx.getClass().getMethod("getFragmentManager"); + fm = (FragmentManager)mth.invoke(ctx);
Cast ctx to Activity and call getFragmentManager directly.
+ } + catch (NoSuchMethodException | NullPointerException | InvocationTargetException | ClassCastException | IllegalAccessException ignored) { + q.add("RegisterFragment: Cannot get Fragment manager from View Context"); + return; + } + q.add(""); + FragmentTransaction ft = fm.beginTransaction(); + ft.add(frag, del);
Call setRetained(true) on fragment, and note that in the documentation? The user can call setRetained(true) themselves, but they may not realize it's necessary to handle Activity re-creation.
+ ft.commitNow(); + } + }); + // NOTE: Because of the use of a BlockingQueue here, this method + // cannot be called while the main UI thread is blocked. + try { + return q.take(); + } catch (InterruptedException e) { + return "RegisterFragment: interrupted"; + } + } + static private native long onCreateView(GioView view); static private native void onDestroyView(long handle); static private native void onStartView(long handle); diff --git a/app/internal/window/handle.go b/app/internal/window/handle.go deleted file mode 100644 index 5d7b1a1..0000000 --- a/app/internal/window/handle.go @@ -1,7 +0,0 @@ -// +build !android - -package window - -var PlatformHandle *Handle - -type Handle struct{} diff --git a/app/internal/window/os_android.c b/app/internal/window/os_android.c index 5150ab6..435350d 100644 --- a/app/internal/window/os_android.c +++ b/app/internal/window/os_android.c @@ -166,3 +166,16 @@ void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes) jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr) { return (*env)->GetArrayLength(env, arr); } + +jstring gio_jni_RegisterFragment(JNIEnv *env, jobject view, jmethodID mid, char* del) { + jstring jdel = (*env)->NewStringUTF(env, del); + return (*env)->CallObjectMethod(env, view, mid, jdel); +} + +const char *gio_jni_GetStringUTFChars(JNIEnv *env, jstring string) { + return (*env)->GetStringUTFChars(env, string, NULL); +} + +void gio_jni_ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf) { + (*env)->ReleaseStringUTFChars(env, string, utf); +} diff --git a/app/internal/window/os_android.go b/app/internal/window/os_android.go index 5cce88d..74d7482 100644 --- a/app/internal/window/os_android.go +++ b/app/internal/window/os_android.go @@ -54,6 +54,7 @@ type window struct { mhideTextInput C.jmethodID mpostFrameCallback C.jmethodID mpostFrameCallbackOnMainThread C.jmethodID + mRegisterFragment C.jmethodID } var dataDirChan = make(chan string, 1) @@ -119,6 +120,7 @@ func onCreateView(env *C.JNIEnv, class C.jclass, view C.jobject) C.jlong { mhideTextInput: jniGetMethodID(env, class, "hideTextInput", "()V"), mpostFrameCallback: jniGetMethodID(env, class, "postFrameCallback", "()V"), mpostFrameCallbackOnMainThread: jniGetMethodID(env, class, "postFrameCallbackOnMainThread", "()V"), + mRegisterFragment: jniGetMethodID(env, class, "registerFragment", "(Ljava/lang/String;)Ljava/lang/String;"), } wopts := <-mainWindow.out w.callbacks = wopts.window @@ -443,6 +445,30 @@ func (w *window) ShowTextInput(show bool) { }) } +type AndroidDriver interface { + RegisterFragment(string) error +} + +func (w *window) RegisterFragment(del string) error { + if w.view == 0 { + return errors.New("RegisterFragment: view is null") + } + ret := "" + runInJVM(func(env *C.JNIEnv) { + cdel := C.CString(del) + defer C.free(unsafe.Pointer(cdel)) + jret := C.gio_jni_RegisterFragment(env, w.view, w.mRegisterFragment, cdel) + utf := C.gio_jni_GetStringUTFChars(env, jret) + ret = C.GoString(utf) + C.gio_jni_ReleaseStringUTFChars(env, jret, utf) + }) + if ret == "" { + return nil + } else { + return errors.New(ret) + } +} + func Main() { } diff --git a/app/internal/window/os_android.h b/app/internal/window/os_android.h index 288d633..d17366c 100644 --- a/app/internal/window/os_android.h +++ b/app/internal/window/os_android.h @@ -17,3 +17,6 @@ __attribute__ ((visibility ("hidden"))) void gio_jni_CallVoidMethod_J(JNIEnv *en __attribute__ ((visibility ("hidden"))) jbyte *gio_jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr); __attribute__ ((visibility ("hidden"))) void gio_jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes); __attribute__ ((visibility ("hidden"))) jsize gio_jni_GetArrayLength(JNIEnv *env, jbyteArray arr); +__attribute__ ((visibility ("hidden"))) jstring gio_jni_RegisterFragment(JNIEnv *env, jobject view, jmethodID mid, char* del); +__attribute__ ((visibility ("hidden"))) const char *gio_jni_GetStringUTFChars(JNIEnv *env, jstring string); +__attribute__ ((visibility ("hidden"))) void gio_jni_ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf); -- 2.16.2
The access to w.driver needs to happen from the w.run goroutine. Change RegisterFragment to pass the fragment name through a channel and send the error back from another.
You should instantiate the class from the UI Thread. The user likely expects the constructor to be able to access UI state.
You only need this type in package app, right? If so, move it to package app and unexport.