Browse Source

Leverage java annotations to simplify the logic used to register the Godot plugin methods.

Fredia Huya-Kouadio 4 years ago
parent
commit
94df08aae1

+ 3 - 1
platform/android/java/lib/src/org/godotengine/godot/FullScreenGodotApp.java

@@ -44,7 +44,7 @@ import androidx.fragment.app.FragmentActivity;
  * It's also a reference implementation for how to setup and use the {@link Godot} fragment
  * within an Android app.
  */
-public abstract class FullScreenGodotApp extends FragmentActivity {
+public abstract class FullScreenGodotApp extends FragmentActivity implements GodotHost {
 
 	@Nullable
 	private Godot godotFragment;
@@ -65,6 +65,8 @@ public abstract class FullScreenGodotApp extends FragmentActivity {
 	public void onNewIntent(Intent intent) {
 		if (godotFragment != null) {
 			godotFragment.onNewIntent(intent);
+		} else {
+			super.onNewIntent(intent);
 		}
 	}
 

+ 56 - 5
platform/android/java/lib/src/org/godotengine/godot/Godot.java

@@ -108,6 +108,7 @@ import java.io.InputStream;
 import java.lang.reflect.Method;
 import java.security.MessageDigest;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
@@ -143,6 +144,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 	// Used to dispatch events to the main thread.
 	private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
 
+	private GodotHost godotHost;
 	private GodotPluginRegistry pluginRegistry;
 
 	static private Intent mCurrentIntent;
@@ -231,7 +233,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 
 		protected void onGLDrawFrame(GL10 gl) {}
 		protected void onGLSurfaceChanged(GL10 gl, int width, int height) {} // singletons will always miss first onGLSurfaceChanged call
-		//protected void onGLSurfaceCreated(GL10 gl, EGLConfig config) {} // singletons won't be ready until first GodotLib.step()
+		// protected void onGLSurfaceCreated(GL10 gl, EGLConfig config) {} // singletons won't be ready until first GodotLib.step()
 
 		public void registerMethods() {}
 	}
@@ -269,6 +271,22 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 	}
 	public ResultCallback result_callback;
 
+	@Override
+	public void onAttach(Context context) {
+		super.onAttach(context);
+		if (getParentFragment() instanceof GodotHost) {
+			godotHost = (GodotHost)getParentFragment();
+		} else if (getActivity() instanceof GodotHost) {
+			godotHost = (GodotHost)getActivity();
+		}
+	}
+
+	@Override
+	public void onDetach() {
+		super.onDetach();
+		godotHost = null;
+	}
+
 	@Override
 	public void onActivityResult(int requestCode, int resultCode, Intent data) {
 		if (result_callback != null) {
@@ -299,6 +317,20 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 		}
 	};
 
+	/**
+	 * Invoked on the render thread when the Godot setup is complete.
+	 */
+	@CallSuper
+	protected void onGodotSetupCompleted() {
+		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
+			plugin.onGodotSetupCompleted();
+		}
+
+		if (godotHost != null) {
+			godotHost.onGodotSetupCompleted();
+		}
+	}
+
 	/**
 	 * Invoked on the render thread when the Godot main loop has started.
 	 */
@@ -307,6 +339,10 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 		for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) {
 			plugin.onGodotMainLoopStarted();
 		}
+
+		if (godotHost != null) {
+			godotHost.onGodotMainLoopStarted();
+		}
 	}
 
 	/**
@@ -409,7 +445,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 					v.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE));
 				} else {
-					//deprecated in API 26
+					// deprecated in API 26
 					v.vibrate(durationMs);
 				}
 			}
@@ -464,6 +500,21 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 
 	@CallSuper
 	protected String[] getCommandLine() {
+		String[] original = parseCommandLine();
+		String[] updated;
+		List<String> hostCommandLine = godotHost != null ? godotHost.getCommandLine() : null;
+		if (hostCommandLine == null || hostCommandLine.isEmpty()) {
+			updated = original;
+		} else {
+			updated = Arrays.copyOf(original, original.length + hostCommandLine.size());
+			for (int i = 0; i < hostCommandLine.size(); i++) {
+				updated[original.length + i] = hostCommandLine.get(i);
+			}
+		}
+		return updated;
+	}
+
+	private String[] parseCommandLine() {
 		InputStream is;
 		try {
 			is = getActivity().getAssets().open("_cl_");
@@ -584,7 +635,7 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 		mClipboard = (ClipboardManager)activity.getSystemService(Context.CLIPBOARD_SERVICE);
 		pluginRegistry = GodotPluginRegistry.initializePluginRegistry(this);
 
-		//check for apk expansion API
+		// check for apk expansion API
 		boolean md5mismatch = false;
 		command_line = getCommandLine();
 		String main_pack_md5 = null;
@@ -642,9 +693,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 			command_line = new_args.toArray(new String[new_args.size()]);
 		}
 		if (use_apk_expansion && main_pack_md5 != null && main_pack_key != null) {
-			//check that environment is ok!
+			// check that environment is ok!
 			if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
-				//show popup and die
+				// show popup and die
 			}
 
 			// Build the full path to the app's expansion files

+ 57 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotHost.java

@@ -0,0 +1,57 @@
+/*************************************************************************/
+/*  GodotHost.java                                                       */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+package org.godotengine.godot;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Denotate a component (e.g: Activity, Fragment) that hosts the {@link Godot} fragment.
+ */
+public interface GodotHost {
+
+	/**
+	 * Provides a set of command line parameters to setup the engine.
+	 */
+	default List<String> getCommandLine() {
+		return Collections.emptyList();
+	}
+
+	/**
+	 * Invoked on the render thread when the Godot setup is complete.
+	 */
+	default void onGodotSetupCompleted() {}
+
+	/**
+	 * Invoked on the render thread when the Godot main loop has started.
+	 */
+	default void onGodotMainLoopStarted() {}
+}

+ 45 - 41
platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java

@@ -46,6 +46,7 @@ import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -109,31 +110,9 @@ public abstract class GodotPlugin {
 	 * This method is invoked on the render thread.
 	 */
 	public final void onRegisterPluginWithGodotNative() {
-		registeredSignals.putAll(registerPluginWithGodotNative(this, new GodotPluginInfoProvider() {
-			@NonNull
-			@Override
-			public String getPluginName() {
-				return GodotPlugin.this.getPluginName();
-			}
-
-			@NonNull
-			@Override
-			public List<String> getPluginMethods() {
-				return GodotPlugin.this.getPluginMethods();
-			}
-
-			@NonNull
-			@Override
-			public Set<SignalInfo> getPluginSignals() {
-				return GodotPlugin.this.getPluginSignals();
-			}
-
-			@NonNull
-			@Override
-			public Set<String> getPluginGDNativeLibrariesPaths() {
-				return GodotPlugin.this.getPluginGDNativeLibrariesPaths();
-			}
-		}));
+		registeredSignals.putAll(
+				registerPluginWithGodotNative(this, getPluginName(), getPluginMethods(), getPluginSignals(),
+						getPluginGDNativeLibrariesPaths()));
 	}
 
 	/**
@@ -141,23 +120,41 @@ public abstract class GodotPlugin {
 	 *
 	 * This method must be invoked on the render thread.
 	 */
-	public static Map<String, SignalInfo> registerPluginWithGodotNative(Object pluginObject, GodotPluginInfoProvider pluginInfoProvider) {
-		nativeRegisterSingleton(pluginInfoProvider.getPluginName(), pluginObject);
+	public static void registerPluginWithGodotNative(Object pluginObject,
+			GodotPluginInfoProvider pluginInfoProvider) {
+		registerPluginWithGodotNative(pluginObject, pluginInfoProvider.getPluginName(),
+				Collections.emptyList(), pluginInfoProvider.getPluginSignals(),
+				pluginInfoProvider.getPluginGDNativeLibrariesPaths());
+
+		// Notify that registration is complete.
+		pluginInfoProvider.onPluginRegistered();
+	}
 
+	private static Map<String, SignalInfo> registerPluginWithGodotNative(Object pluginObject,
+			String pluginName, List<String> pluginMethods, Set<SignalInfo> pluginSignals,
+			Set<String> pluginGDNativeLibrariesPaths) {
+		nativeRegisterSingleton(pluginName, pluginObject);
+
+		Set<Method> filteredMethods = new HashSet<>();
 		Class clazz = pluginObject.getClass();
+
 		Method[] methods = clazz.getDeclaredMethods();
 		for (Method method : methods) {
-			boolean found = false;
-
-			for (String s : pluginInfoProvider.getPluginMethods()) {
-				if (s.equals(method.getName())) {
-					found = true;
-					break;
+			// Check if the method is annotated with {@link UsedByGodot}.
+			if (method.getAnnotation(UsedByGodot.class) != null) {
+				filteredMethods.add(method);
+			} else {
+				// For backward compatibility, process the methods from the given <pluginMethods> argument.
+				for (String methodName : pluginMethods) {
+					if (methodName.equals(method.getName())) {
+						filteredMethods.add(method);
+						break;
+					}
 				}
 			}
-			if (!found)
-				continue;
+		}
 
+		for (Method method : filteredMethods) {
 			List<String> ptr = new ArrayList<>();
 
 			Class[] paramTypes = method.getParameterTypes();
@@ -168,21 +165,20 @@ public abstract class GodotPlugin {
 			String[] pt = new String[ptr.size()];
 			ptr.toArray(pt);
 
-			nativeRegisterMethod(pluginInfoProvider.getPluginName(), method.getName(), method.getReturnType().getName(), pt);
+			nativeRegisterMethod(pluginName, method.getName(), method.getReturnType().getName(), pt);
 		}
 
 		// Register the signals for this plugin.
 		Map<String, SignalInfo> registeredSignals = new HashMap<>();
-		for (SignalInfo signalInfo : pluginInfoProvider.getPluginSignals()) {
+		for (SignalInfo signalInfo : pluginSignals) {
 			String signalName = signalInfo.getName();
-			nativeRegisterSignal(pluginInfoProvider.getPluginName(), signalName, signalInfo.getParamTypesNames());
+			nativeRegisterSignal(pluginName, signalName, signalInfo.getParamTypesNames());
 			registeredSignals.put(signalName, signalInfo);
 		}
 
 		// Get the list of gdnative libraries to register.
-		Set<String> gdnativeLibrariesPaths = pluginInfoProvider.getPluginGDNativeLibrariesPaths();
-		if (!gdnativeLibrariesPaths.isEmpty()) {
-			nativeRegisterGDNativeLibraries(gdnativeLibrariesPaths.toArray(new String[0]));
+		if (!pluginGDNativeLibrariesPaths.isEmpty()) {
+			nativeRegisterGDNativeLibraries(pluginGDNativeLibrariesPaths.toArray(new String[0]));
 		}
 
 		return registeredSignals;
@@ -235,6 +231,11 @@ public abstract class GodotPlugin {
 	 */
 	public boolean onMainBackPressed() { return false; }
 
+	/**
+	 * Invoked on the render thread when the Godot setup is complete.
+	 */
+	public void onGodotSetupCompleted() {}
+
 	/**
 	 * Invoked on the render thread when the Godot main loop has started.
 	 */
@@ -266,8 +267,11 @@ public abstract class GodotPlugin {
 
 	/**
 	 * Returns the list of methods to be exposed to Godot.
+	 *
+	 * @deprecated Used the {@link UsedByGodot} annotation instead.
 	 */
 	@NonNull
+	@Deprecated
 	public List<String> getPluginMethods() {
 		return Collections.emptyList();
 	}

+ 14 - 9
platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginInfoProvider.java

@@ -32,7 +32,7 @@ package org.godotengine.godot.plugin;
 
 import androidx.annotation.NonNull;
 
-import java.util.List;
+import java.util.Collections;
 import java.util.Set;
 
 /**
@@ -46,17 +46,13 @@ public interface GodotPluginInfoProvider {
 	@NonNull
 	String getPluginName();
 
-	/**
-	 * Returns the list of methods to be exposed to Godot.
-	 */
-	@NonNull
-	List<String> getPluginMethods();
-
 	/**
 	 * Returns the list of signals to be exposed to Godot.
 	 */
 	@NonNull
-	Set<SignalInfo> getPluginSignals();
+	default Set<SignalInfo> getPluginSignals() {
+		return Collections.emptySet();
+	}
 
 	/**
 	 * Returns the paths for the plugin's gdnative libraries (if any).
@@ -64,5 +60,14 @@ public interface GodotPluginInfoProvider {
 	 * The paths must be relative to the 'assets' directory and point to a '*.gdnlib' file.
 	 */
 	@NonNull
-	Set<String> getPluginGDNativeLibrariesPaths();
+	default Set<String> getPluginGDNativeLibrariesPaths() {
+		return Collections.emptySet();
+	}
+
+	/**
+	 * This is invoked on the render thread when the plugin described by this instance has been
+	 * registered.
+	 */
+	default void onPluginRegistered() {
+	}
 }

+ 45 - 0
platform/android/java/lib/src/org/godotengine/godot/plugin/UsedByGodot.java

@@ -0,0 +1,45 @@
+/*************************************************************************/
+/*  UsedByGodot.java                                                     */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+package org.godotengine.godot.plugin;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to indicate a method is being invoked from the Godot game logic.
+ *
+ * At runtime, annotated plugin methods are detected and automatically registered.
+ */
+@Target({ ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface UsedByGodot {}

+ 3 - 2
platform/android/java_godot_lib_jni.cpp

@@ -201,7 +201,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jc
 	}
 
 	if (err != OK) {
-		return; //should exit instead and print the error
+		return; // should exit instead and print the error
 	}
 
 	java_class_wrapper = memnew(JavaClassWrapper(godot_java->get_activity()));
@@ -252,9 +252,10 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_step(JNIEnv *env, jcl
 
 	if (step == 1) {
 		if (!Main::start()) {
-			return; //should exit instead and print the error
+			return; // should exit instead and print the error
 		}
 
+		godot_java->on_godot_setup_completed(env);
 		os_android->main_loop_begin();
 		godot_java->on_godot_main_loop_started(env);
 		++step;

+ 20 - 7
platform/android/java_godot_wrapper.cpp

@@ -74,6 +74,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 	_is_activity_resumed = p_env->GetMethodID(godot_class, "isActivityResumed", "()Z");
 	_vibrate = p_env->GetMethodID(godot_class, "vibrate", "(I)V");
 	_get_input_fallback_mapping = p_env->GetMethodID(godot_class, "getInputFallbackMapping", "()Ljava/lang/String;");
+	_on_godot_setup_completed = p_env->GetMethodID(godot_class, "onGodotSetupCompleted", "()V");
 	_on_godot_main_loop_started = p_env->GetMethodID(godot_class, "onGodotMainLoopStarted", "()V");
 
 	// get some Activity method pointers...
@@ -117,11 +118,21 @@ void GodotJavaWrapper::gfx_init(bool gl2) {
 }
 
 void GodotJavaWrapper::on_video_init(JNIEnv *p_env) {
-	if (_on_video_init)
+	if (_on_video_init) {
 		if (p_env == NULL)
 			p_env = get_jni_env();
 
-	p_env->CallVoidMethod(godot_instance, _on_video_init);
+		p_env->CallVoidMethod(godot_instance, _on_video_init);
+	}
+}
+
+void GodotJavaWrapper::on_godot_setup_completed(JNIEnv *p_env) {
+	if (_on_godot_setup_completed) {
+		if (p_env == NULL) {
+			p_env = get_jni_env();
+		}
+		p_env->CallVoidMethod(godot_instance, _on_godot_setup_completed);
+	}
 }
 
 void GodotJavaWrapper::on_godot_main_loop_started(JNIEnv *p_env) {
@@ -129,24 +140,26 @@ void GodotJavaWrapper::on_godot_main_loop_started(JNIEnv *p_env) {
 		if (p_env == NULL) {
 			p_env = get_jni_env();
 		}
+		p_env->CallVoidMethod(godot_instance, _on_godot_main_loop_started);
 	}
-	p_env->CallVoidMethod(godot_instance, _on_godot_main_loop_started);
 }
 
 void GodotJavaWrapper::restart(JNIEnv *p_env) {
-	if (_restart)
+	if (_restart) {
 		if (p_env == NULL)
 			p_env = get_jni_env();
 
-	p_env->CallVoidMethod(godot_instance, _restart);
+		p_env->CallVoidMethod(godot_instance, _restart);
+	}
 }
 
 void GodotJavaWrapper::force_quit(JNIEnv *p_env) {
-	if (_finish)
+	if (_finish) {
 		if (p_env == NULL)
 			p_env = get_jni_env();
 
-	p_env->CallVoidMethod(godot_instance, _finish);
+		p_env->CallVoidMethod(godot_instance, _finish);
+	}
 }
 
 void GodotJavaWrapper::set_keep_screen_on(bool p_enabled) {

+ 2 - 0
platform/android/java_godot_wrapper.h

@@ -63,6 +63,7 @@ private:
 	jmethodID _is_activity_resumed = 0;
 	jmethodID _vibrate = 0;
 	jmethodID _get_input_fallback_mapping = 0;
+	jmethodID _on_godot_setup_completed = 0;
 	jmethodID _on_godot_main_loop_started = 0;
 	jmethodID _get_class_loader = 0;
 
@@ -77,6 +78,7 @@ public:
 
 	void gfx_init(bool gl2);
 	void on_video_init(JNIEnv *p_env = NULL);
+	void on_godot_setup_completed(JNIEnv *p_env = NULL);
 	void on_godot_main_loop_started(JNIEnv *p_env = NULL);
 	void restart(JNIEnv *p_env = NULL);
 	void force_quit(JNIEnv *p_env = NULL);