~eliasnaur/gio

app: add RegisterFragment method on *Window for Android v1 PROPOSED

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 
necessary to move this to the UI thread.
Next
BTW, please send further patches to the ~eliasnaur/gio-patches mailing
list, which I recently
added to separate discussion from patches.

Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH] app: add RegisterFragment method on *Window for Android Export this patch

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) {
			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";
		}

		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);
				}
				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);
				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.