Browse Source

Additional fixes and improvements to JavaClassWrapper

- Fix crashing bug when invoking class constructor with parameters
- Add support for accessing class constants
- Add support for Godot Callable arguments. A Godot Callable can be wrapped by a Java Runnable to allow Java logic to run arbitrary Godot lambdas
- Automatically convert java.lang.CharSequence to Godot String as needed
- Code cleanup
Fredia Huya-Kouadio 9 months ago
parent
commit
23cea1b9d2

+ 1 - 0
platform/android/SCsub

@@ -28,6 +28,7 @@ android_files = [
     "display_server_android.cpp",
     "plugin/godot_plugin_jni.cpp",
     "rendering_context_driver_vulkan_android.cpp",
+    "variant/callable_jni.cpp",
 ]
 
 env_android = env.Clone()

+ 3 - 0
platform/android/api/api.cpp

@@ -73,6 +73,9 @@ void JavaClassWrapper::_bind_methods() {
 }
 
 #if !defined(ANDROID_ENABLED)
+bool JavaClass::_get(const StringName &p_name, Variant &r_ret) const {
+	return false;
+}
 
 Variant JavaClass::callp(const StringName &, const Variant **, int, Callable::CallError &) {
 	return Variant();

+ 9 - 0
platform/android/api/java_class_wrapper.h

@@ -58,6 +58,8 @@ class JavaClass : public RefCounted {
 		ARG_TYPE_FLOAT,
 		ARG_TYPE_DOUBLE,
 		ARG_TYPE_STRING, //special case
+		ARG_TYPE_CHARSEQUENCE,
+		ARG_TYPE_CALLABLE,
 		ARG_TYPE_CLASS,
 		ARG_ARRAY_BIT = 1 << 16,
 		ARG_NUMBER_CLASS_BIT = 1 << 17,
@@ -123,8 +125,12 @@ class JavaClass : public RefCounted {
 				likelihood = 0.5;
 				break;
 			case ARG_TYPE_STRING:
+			case ARG_TYPE_CHARSEQUENCE:
 				r_type = Variant::STRING;
 				break;
+			case ARG_TYPE_CALLABLE:
+				r_type = Variant::CALLABLE;
+				break;
 			case ARG_TYPE_CLASS:
 				r_type = Variant::OBJECT;
 				break;
@@ -163,9 +169,11 @@ class JavaClass : public RefCounted {
 				likelihood = 0.5;
 				break;
 			case ARG_ARRAY_BIT | ARG_TYPE_STRING:
+			case ARG_ARRAY_BIT | ARG_TYPE_CHARSEQUENCE:
 				r_type = Variant::PACKED_STRING_ARRAY;
 				break;
 			case ARG_ARRAY_BIT | ARG_TYPE_CLASS:
+			case ARG_ARRAY_BIT | ARG_TYPE_CALLABLE:
 				r_type = Variant::ARRAY;
 				break;
 		}
@@ -185,6 +193,7 @@ class JavaClass : public RefCounted {
 
 protected:
 	static void _bind_methods();
+	bool _get(const StringName &p_name, Variant &r_ret) const;
 
 public:
 	virtual Variant callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) override;

+ 1 - 1
platform/android/dir_access_jandroid.cpp

@@ -30,7 +30,7 @@
 
 #include "dir_access_jandroid.h"
 
-#include "string_android.h"
+#include "jni_utils.h"
 #include "thread_jandroid.h"
 
 #include "core/string/print_string.h"

+ 13 - 2
platform/android/java/lib/src/org/godotengine/godot/GodotLib.java

@@ -35,6 +35,7 @@ import org.godotengine.godot.io.directory.DirectoryAccessHandler;
 import org.godotengine.godot.io.file.FileAccessHandler;
 import org.godotengine.godot.tts.GodotTTS;
 import org.godotengine.godot.utils.GodotNetUtils;
+import org.godotengine.godot.variant.Callable;
 
 import android.app.Activity;
 import android.content.res.AssetManager;
@@ -200,16 +201,26 @@ public class GodotLib {
 	 * @param p_id Id of the Godot object to invoke
 	 * @param p_method Name of the method to invoke
 	 * @param p_params Parameters to use for method invocation
+	 *
+	 * @deprecated Use {@link Callable#call(long, String, Object...)} instead.
 	 */
-	public static native void callobject(long p_id, String p_method, Object[] p_params);
+	@Deprecated
+	public static void callobject(long p_id, String p_method, Object[] p_params) {
+		Callable.call(p_id, p_method, p_params);
+	}
 
 	/**
 	 * Invoke method |p_method| on the Godot object specified by |p_id| during idle time.
 	 * @param p_id Id of the Godot object to invoke
 	 * @param p_method Name of the method to invoke
 	 * @param p_params Parameters to use for method invocation
+	 *
+	 * @deprecated Use {@link Callable#callDeferred(long, String, Object...)} instead.
 	 */
-	public static native void calldeferred(long p_id, String p_method, Object[] p_params);
+	@Deprecated
+	public static void calldeferred(long p_id, String p_method, Object[] p_params) {
+		Callable.callDeferred(p_id, p_method, p_params);
+	}
 
 	/**
 	 * Forward the results from a permission request.

+ 39 - 1
platform/android/java/lib/src/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt

@@ -31,6 +31,7 @@
 package org.godotengine.godot.plugin
 
 import org.godotengine.godot.Godot
+import org.godotengine.godot.variant.Callable
 
 /**
  * Provides access to the Android runtime capabilities.
@@ -38,7 +39,7 @@ import org.godotengine.godot.Godot
  * For example, from gdscript, developers can use [getApplicationContext] to access system services
  * and check if the device supports vibration.
  *
- * var android_runtime = Engine.get_singleton("AndroidRuntime")
+ * 	var android_runtime = Engine.get_singleton("AndroidRuntime")
  * 	if android_runtime:
  * 		print("Checking if the device supports vibration")
  * 		var vibrator_service = android_runtime.getApplicationContext().getSystemService("vibrator")
@@ -51,13 +52,50 @@ import org.godotengine.godot.Godot
  * 			printerr("Unable to retrieve the vibrator service")
  * 	else:
  * 		printerr("Couldn't find AndroidRuntime singleton")
+ *
+ *
+ * Or it can be used to display an Android native toast from gdscript
+ *
+ * 	var android_runtime = Engine.get_singleton("AndroidRuntime")
+ * 	if android_runtime:
+ * 		var activity = android_runtime.getActivity()
+ *
+ * 		var toastCallable = func ():
+ * 			var ToastClass = JavaClassWrapper.wrap("android.widget.Toast")
+ * 			ToastClass.makeText(activity, "This is a test", ToastClass.LENGTH_LONG).show()
+ *
+ * 		activity.runOnUiThread(android_runtime.createRunnableFromGodotCallable(toastCallable))
+ * 	else:
+ * 		printerr("Unable to access android runtime")
  */
 class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) {
 	override fun getPluginName() = "AndroidRuntime"
 
+	/**
+	 * Provides access to the application context to GDScript
+	 */
 	@UsedByGodot
 	fun getApplicationContext() = activity?.applicationContext
 
+	/**
+	 * Provides access to the host activity to GDScript
+	 */
 	@UsedByGodot
 	override fun getActivity() = super.getActivity()
+
+	/**
+	 * Utility method used to create [Runnable] from Godot [Callable].
+	 */
+	@UsedByGodot
+	fun createRunnableFromGodotCallable(godotCallable: Callable): Runnable {
+		return Runnable { godotCallable.call() }
+	}
+
+	/**
+	 * Utility method used to create [java.util.concurrent.Callable] from Godot [Callable].
+	 */
+	@UsedByGodot
+	fun createCallableFromGodotCallable(godotCallable: Callable): java.util.concurrent.Callable<Any> {
+		return java.util.concurrent.Callable { godotCallable.call() }
+	}
 }

+ 94 - 0
platform/android/java/lib/src/org/godotengine/godot/variant/Callable.kt

@@ -0,0 +1,94 @@
+/**************************************************************************/
+/*  Callable.kt                                                           */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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.variant
+
+import androidx.annotation.Keep
+
+/**
+ * Android version of a Godot built-in Callable type representing a method or a standalone function.
+ */
+@Keep
+class Callable private constructor(private val nativeCallablePointer: Long) {
+
+	companion object {
+		/**
+		 * Invoke method [methodName] on the Godot object specified by [godotObjectId]
+		 */
+		@JvmStatic
+		fun call(godotObjectId: Long, methodName: String, vararg methodParameters: Any): Any? {
+			return nativeCallObject(godotObjectId, methodName, methodParameters)
+		}
+
+		/**
+		 * Invoke method [methodName] on the Godot object specified by [godotObjectId] during idle time.
+		 */
+		@JvmStatic
+		fun callDeferred(godotObjectId: Long, methodName: String, vararg methodParameters: Any) {
+			nativeCallObjectDeferred(godotObjectId, methodName, methodParameters)
+		}
+
+		@JvmStatic
+		private external fun nativeCall(pointer: Long, params: Array<out Any>): Any?
+
+		@JvmStatic
+		private external fun nativeCallObject(godotObjectId: Long, methodName: String, params: Array<out Any>): Any?
+
+		@JvmStatic
+		private external fun nativeCallObjectDeferred(godotObjectId: Long, methodName: String, params: Array<out Any>)
+
+		@JvmStatic
+		private external fun releaseNativePointer(nativePointer: Long)
+	}
+
+	/**
+	 * Calls the method represented by this [Callable]. Arguments can be passed and should match the method's signature.
+	 */
+	internal fun call(vararg params: Any): Any? {
+		if (nativeCallablePointer == 0L) {
+			return null
+		}
+
+		return nativeCall(nativeCallablePointer, params)
+	}
+
+	/**
+	 * Used to provide access to the native callable pointer to the native logic.
+	 */
+	private fun getNativePointer() = nativeCallablePointer
+
+	/** Note that [finalize] is deprecated and shouldn't be used, unfortunately its replacement,
+	 * [java.lang.ref.Cleaner], is only available on Android api 33 and higher.
+	 * So we resort to using it for the time being until our min api catches up to api 33.
+	 **/
+	protected fun finalize() {
+		releaseNativePointer(nativeCallablePointer)
+	}
+}

+ 92 - 5
platform/android/java_class_wrapper.cpp

@@ -30,7 +30,7 @@
 
 #include "api/java_class_wrapper.h"
 
-#include "string_android.h"
+#include "jni_utils.h"
 #include "thread_jandroid.h"
 
 bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error, Variant &ret) {
@@ -96,11 +96,17 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method,
 						arg_expected = Variant::FLOAT;
 					}
 				} break;
-				case ARG_TYPE_STRING: {
+				case ARG_TYPE_STRING:
+				case ARG_TYPE_CHARSEQUENCE: {
 					if (!p_args[i]->is_string()) {
 						arg_expected = Variant::STRING;
 					}
 				} break;
+				case ARG_TYPE_CALLABLE: {
+					if (p_args[i]->get_type() != Variant::CALLABLE) {
+						arg_expected = Variant::CALLABLE;
+					}
+				} break;
 				case ARG_TYPE_CLASS: {
 					if (p_args[i]->get_type() != Variant::OBJECT && p_args[i]->get_type() != Variant::NIL) {
 						arg_expected = Variant::OBJECT;
@@ -265,12 +271,18 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method,
 				argv[i].l = obj;
 				to_free.push_back(obj);
 			} break;
-			case ARG_TYPE_STRING: {
+			case ARG_TYPE_STRING:
+			case ARG_TYPE_CHARSEQUENCE: {
 				String s = *p_args[i];
 				jstring jStr = env->NewStringUTF(s.utf8().get_data());
 				argv[i].l = jStr;
 				to_free.push_back(jStr);
 			} break;
+			case ARG_TYPE_CALLABLE: {
+				jobject jcallable = callable_to_jcallable(env, *p_args[i]);
+				argv[i].l = jcallable;
+				to_free.push_back(jcallable);
+			} break;
 			case ARG_TYPE_CLASS: {
 				Ref<JavaObject> jo = *p_args[i];
 				if (jo.is_valid()) {
@@ -367,7 +379,8 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method,
 				to_free.push_back(a);
 
 			} break;
-			case ARG_ARRAY_BIT | ARG_TYPE_STRING: {
+			case ARG_ARRAY_BIT | ARG_TYPE_STRING:
+			case ARG_ARRAY_BIT | ARG_TYPE_CHARSEQUENCE: {
 				Array arr = *p_args[i];
 				jobjectArray a = env->NewObjectArray(arr.size(), env->FindClass("java/lang/String"), nullptr);
 				for (int j = 0; j < arr.size(); j++) {
@@ -380,6 +393,19 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method,
 				argv[i].l = a;
 				to_free.push_back(a);
 			} break;
+			case ARG_ARRAY_BIT | ARG_TYPE_CALLABLE: {
+				Array arr = *p_args[i];
+				jobjectArray jarr = env->NewObjectArray(arr.size(), env->FindClass("org/godotengine/godot/variant/Callable"), nullptr);
+				for (int j = 0; j < arr.size(); j++) {
+					Variant callable = arr[j];
+					jobject jcallable = callable_to_jcallable(env, callable);
+					env->SetObjectArrayElement(jarr, j, jcallable);
+					to_free.push_back(jcallable);
+				}
+
+				argv[i].l = jarr;
+				to_free.push_back(jarr);
+			} break;
 			case ARG_ARRAY_BIT | ARG_TYPE_CLASS: {
 				argv[i].l = nullptr;
 			} break;
@@ -463,7 +489,7 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method,
 		default: {
 			jobject obj;
 			if (method->_constructor) {
-				obj = env->NewObject(_class, method->method, argv);
+				obj = env->NewObjectA(_class, method->method, argv);
 			} else if (method->_static) {
 				obj = env->CallStaticObjectMethodA(_class, method->method, argv);
 			} else {
@@ -491,6 +517,15 @@ bool JavaClass::_call_method(JavaObject *p_instance, const StringName &p_method,
 	return success;
 }
 
+bool JavaClass::_get(const StringName &p_name, Variant &r_ret) const {
+	if (constant_map.has(p_name)) {
+		r_ret = constant_map[p_name];
+		return true;
+	}
+
+	return false;
+}
+
 Variant JavaClass::callp(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) {
 	Variant ret;
 
@@ -706,6 +741,12 @@ bool JavaClassWrapper::_get_type_sig(JNIEnv *env, jobject obj, uint32_t &sig, St
 	} else if (str_type == "java.lang.String") {
 		t |= JavaClass::ARG_TYPE_STRING;
 		strsig += "Ljava/lang/String;";
+	} else if (str_type == "java.lang.CharSequence") {
+		t |= JavaClass::ARG_TYPE_CHARSEQUENCE;
+		strsig += "Ljava/lang/CharSequence;";
+	} else if (str_type == "org.godotengine.godot.variant.Callable") {
+		t |= JavaClass::ARG_TYPE_CALLABLE;
+		strsig += "Lorg/godotengine/godot/variant/Callable;";
 	} else if (str_type == "java.lang.Boolean") {
 		t |= JavaClass::ARG_TYPE_BOOLEAN | JavaClass::ARG_NUMBER_CLASS_BIT;
 		strsig += "Ljava/lang/Boolean;";
@@ -793,6 +834,14 @@ bool JavaClass::_convert_object_to_variant(JNIEnv *env, jobject obj, Variant &va
 			var = jstring_to_string((jstring)obj, env);
 			return true;
 		} break;
+		case ARG_TYPE_CHARSEQUENCE: {
+			var = charsequence_to_string(env, obj);
+			return true;
+		} break;
+		case ARG_TYPE_CALLABLE: {
+			var = jcallable_to_callable(env, obj);
+			return true;
+		} break;
 		case ARG_TYPE_CLASS: {
 			jclass java_class = env->GetObjectClass(obj);
 			Ref<JavaClass> java_class_wrapped = JavaClassWrapper::singleton->wrap_jclass(java_class);
@@ -1113,6 +1162,44 @@ bool JavaClass::_convert_object_to_variant(JNIEnv *env, jobject obj, Variant &va
 			var = ret;
 			return true;
 		} break;
+		case ARG_ARRAY_BIT | ARG_TYPE_CHARSEQUENCE: {
+			Array ret;
+			jobjectArray arr = (jobjectArray)obj;
+
+			int count = env->GetArrayLength(arr);
+
+			for (int i = 0; i < count; i++) {
+				jobject o = env->GetObjectArrayElement(arr, i);
+				if (!o) {
+					ret.push_back(Variant());
+				} else {
+					String val = charsequence_to_string(env, o);
+					ret.push_back(val);
+				}
+				env->DeleteLocalRef(o);
+			}
+
+			var = ret;
+			return true;
+		} break;
+		case ARG_ARRAY_BIT | ARG_TYPE_CALLABLE: {
+			Array ret;
+			jobjectArray jarr = (jobjectArray)obj;
+			int count = env->GetArrayLength(jarr);
+			for (int i = 0; i < count; i++) {
+				jobject o = env->GetObjectArrayElement(jarr, i);
+				if (!o) {
+					ret.push_back(Variant());
+				} else {
+					Callable callable = jcallable_to_callable(env, o);
+					ret.push_back(callable);
+				}
+				env->DeleteLocalRef(o);
+			}
+
+			var = ret;
+			return true;
+		} break;
 		case ARG_ARRAY_BIT | ARG_TYPE_CLASS: {
 		} break;
 	}

+ 1 - 1
platform/android/java_godot_io_wrapper.h

@@ -31,7 +31,7 @@
 #ifndef JAVA_GODOT_IO_WRAPPER_H
 #define JAVA_GODOT_IO_WRAPPER_H
 
-#include "string_android.h"
+#include "jni_utils.h"
 
 #include "core/math/rect2i.h"
 #include "core/variant/typed_array.h"

+ 0 - 46
platform/android/java_godot_lib_jni.cpp

@@ -42,7 +42,6 @@
 #include "net_socket_android.h"
 #include "os_android.h"
 #include "plugin/godot_plugin_jni.h"
-#include "string_android.h"
 #include "thread_jandroid.h"
 #include "tts_android.h"
 
@@ -488,51 +487,6 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getEditorSetting(J
 	return env->NewStringUTF(editor_setting_value.utf8().get_data());
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params) {
-	Object *obj = ObjectDB::get_instance(ObjectID(ID));
-	ERR_FAIL_NULL(obj);
-
-	String str_method = jstring_to_string(method, env);
-
-	int count = env->GetArrayLength(params);
-
-	Variant *vlist = (Variant *)alloca(sizeof(Variant) * count);
-	const Variant **vptr = (const Variant **)alloca(sizeof(Variant *) * count);
-
-	for (int i = 0; i < count; i++) {
-		jobject jobj = env->GetObjectArrayElement(params, i);
-		ERR_FAIL_NULL(jobj);
-		memnew_placement(&vlist[i], Variant(_jobject_to_variant(env, jobj)));
-		vptr[i] = &vlist[i];
-		env->DeleteLocalRef(jobj);
-	}
-
-	Callable::CallError err;
-	obj->callp(str_method, vptr, count, err);
-}
-
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params) {
-	Object *obj = ObjectDB::get_instance(ObjectID(ID));
-	ERR_FAIL_NULL(obj);
-
-	String str_method = jstring_to_string(method, env);
-
-	int count = env->GetArrayLength(params);
-
-	Variant *args = (Variant *)alloca(sizeof(Variant) * count);
-	const Variant **argptrs = (const Variant **)alloca(sizeof(Variant *) * count);
-
-	for (int i = 0; i < count; i++) {
-		jobject jobj = env->GetObjectArrayElement(params, i);
-		ERR_FAIL_NULL(jobj);
-		memnew_placement(&args[i], Variant(_jobject_to_variant(env, jobj)));
-		argptrs[i] = &args[i];
-		env->DeleteLocalRef(jobj);
-	}
-
-	Callable(obj, str_method).call_deferredp(argptrs, count);
-}
-
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz) {
 	DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
 	if (ds) {

+ 0 - 2
platform/android/java_godot_lib_jni.h

@@ -62,8 +62,6 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env,
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz);
 JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jclass clazz, jstring path);
 JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getEditorSetting(JNIEnv *env, jclass clazz, jstring p_setting_key);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_callobject(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_calldeferred(JNIEnv *env, jclass clazz, jlong ID, jstring method, jobjectArray params);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz);

+ 1 - 1
platform/android/java_godot_view_wrapper.h

@@ -31,7 +31,7 @@
 #ifndef JAVA_GODOT_VIEW_WRAPPER_H
 #define JAVA_GODOT_VIEW_WRAPPER_H
 
-#include "string_android.h"
+#include "jni_utils.h"
 
 #include "core/math/vector2.h"
 

+ 0 - 1
platform/android/java_godot_wrapper.h

@@ -32,7 +32,6 @@
 #define JAVA_GODOT_WRAPPER_H
 
 #include "java_godot_view_wrapper.h"
-#include "string_android.h"
 
 #include "core/math/color.h"
 #include "core/templates/list.h"

+ 82 - 35
platform/android/jni_utils.cpp

@@ -32,6 +32,57 @@
 
 #include "api/java_class_wrapper.h"
 
+jobject callable_to_jcallable(JNIEnv *p_env, const Variant &p_callable) {
+	ERR_FAIL_NULL_V(p_env, nullptr);
+	if (p_callable.get_type() != Variant::CALLABLE) {
+		return nullptr;
+	}
+
+	Variant *callable_jcopy = memnew(Variant(p_callable));
+
+	jclass bclass = p_env->FindClass("org/godotengine/godot/variant/Callable");
+	jmethodID ctor = p_env->GetMethodID(bclass, "<init>", "(J)V");
+	jobject jcallable = p_env->NewObject(bclass, ctor, reinterpret_cast<int64_t>(callable_jcopy));
+	p_env->DeleteLocalRef(bclass);
+
+	return jcallable;
+}
+
+Callable jcallable_to_callable(JNIEnv *p_env, jobject p_jcallable_obj) {
+	ERR_FAIL_NULL_V(p_env, Callable());
+
+	const Variant *callable_variant = nullptr;
+	jclass callable_class = p_env->FindClass("org/godotengine/godot/variant/Callable");
+	if (callable_class && p_env->IsInstanceOf(p_jcallable_obj, callable_class)) {
+		jmethodID get_native_pointer = p_env->GetMethodID(callable_class, "getNativePointer", "()J");
+		jlong native_callable = p_env->CallLongMethod(p_jcallable_obj, get_native_pointer);
+
+		callable_variant = reinterpret_cast<const Variant *>(native_callable);
+	}
+
+	p_env->DeleteLocalRef(callable_class);
+
+	ERR_FAIL_NULL_V(callable_variant, Callable());
+	return *callable_variant;
+}
+
+String charsequence_to_string(JNIEnv *p_env, jobject p_charsequence) {
+	ERR_FAIL_NULL_V(p_env, String());
+
+	String result;
+	jclass bclass = p_env->FindClass("java/lang/CharSequence");
+	if (bclass && p_env->IsInstanceOf(p_charsequence, bclass)) {
+		jmethodID to_string = p_env->GetMethodID(bclass, "toString", "()Ljava/lang/String;");
+		jstring obj_string = (jstring)p_env->CallObjectMethod(p_charsequence, to_string);
+
+		result = jstring_to_string(obj_string, p_env);
+		p_env->DeleteLocalRef(obj_string);
+	}
+
+	p_env->DeleteLocalRef(bclass);
+	return result;
+}
+
 jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_arg, bool force_jobject) {
 	jvalret v;
 
@@ -100,6 +151,12 @@ jvalret _variant_to_jvalue(JNIEnv *env, Variant::Type p_type, const Variant *p_a
 
 		} break;
 
+		case Variant::CALLABLE: {
+			jobject jcallable = callable_to_jcallable(env, *p_arg);
+			v.val.l = jcallable;
+			v.obj = jcallable;
+		} break;
+
 		case Variant::DICTIONARY: {
 			Dictionary dict = *p_arg;
 			jclass dclass = env->FindClass("org/godotengine/godot/Dictionary");
@@ -234,6 +291,10 @@ Variant _jobject_to_variant(JNIEnv *env, jobject obj) {
 		return jstring_to_string((jstring)obj, env);
 	}
 
+	if (name == "java.lang.CharSequence") {
+		return charsequence_to_string(env, obj);
+	}
+
 	if (name == "[Ljava.lang.String;") {
 		jobjectArray arr = (jobjectArray)obj;
 		int stringCount = env->GetArrayLength(arr);
@@ -248,6 +309,20 @@ Variant _jobject_to_variant(JNIEnv *env, jobject obj) {
 		return sarr;
 	}
 
+	if (name == "[Ljava.lang.CharSequence;") {
+		jobjectArray arr = (jobjectArray)obj;
+		int stringCount = env->GetArrayLength(arr);
+		Vector<String> sarr;
+
+		for (int i = 0; i < stringCount; i++) {
+			jobject charsequence = env->GetObjectArrayElement(arr, i);
+			sarr.push_back(charsequence_to_string(env, charsequence));
+			env->DeleteLocalRef(charsequence);
+		}
+
+		return sarr;
+	}
+
 	if (name == "java.lang.Boolean") {
 		jmethodID boolValue = env->GetMethodID(c, "booleanValue", "()Z");
 		bool ret = env->CallBooleanMethod(obj, boolValue);
@@ -370,6 +445,10 @@ Variant _jobject_to_variant(JNIEnv *env, jobject obj) {
 		return ret;
 	}
 
+	if (name == "org.godotengine.godot.variant.Callable") {
+		return jcallable_to_callable(env, obj);
+	}
+
 	Ref<JavaObject> generic_object(memnew(JavaObject(JavaClassWrapper::get_singleton()->wrap(name), obj)));
 
 	env->DeleteLocalRef(c);
@@ -389,13 +468,16 @@ Variant::Type get_jni_type(const String &p_type) {
 		{ "float", Variant::FLOAT },
 		{ "double", Variant::FLOAT },
 		{ "java.lang.String", Variant::STRING },
+		{ "java.lang.CharSequence", Variant::STRING },
 		{ "[I", Variant::PACKED_INT32_ARRAY },
 		{ "[J", Variant::PACKED_INT64_ARRAY },
 		{ "[B", Variant::PACKED_BYTE_ARRAY },
 		{ "[F", Variant::PACKED_FLOAT32_ARRAY },
 		{ "[D", Variant::PACKED_FLOAT64_ARRAY },
 		{ "[Ljava.lang.String;", Variant::PACKED_STRING_ARRAY },
+		{ "[Ljava.lang.CharSequence;", Variant::PACKED_STRING_ARRAY },
 		{ "org.godotengine.godot.Dictionary", Variant::DICTIONARY },
+		{ "org.godotengine.godot.variant.Callable", Variant::CALLABLE },
 		{ nullptr, Variant::NIL }
 	};
 
@@ -411,38 +493,3 @@ Variant::Type get_jni_type(const String &p_type) {
 
 	return Variant::OBJECT;
 }
-
-String get_jni_sig(const String &p_type) {
-	static struct {
-		const char *name;
-		const char *sig;
-	} _type_to_vtype[] = {
-		{ "void", "V" },
-		{ "boolean", "Z" },
-		{ "int", "I" },
-		{ "long", "J" },
-		{ "float", "F" },
-		{ "double", "D" },
-		{ "java.lang.String", "Ljava/lang/String;" },
-		{ "org.godotengine.godot.Dictionary", "Lorg/godotengine/godot/Dictionary;" },
-		{ "[I", "[I" },
-		{ "[J", "[J" },
-		{ "[B", "[B" },
-		{ "[F", "[F" },
-		{ "[D", "[D" },
-		{ "[Ljava.lang.String;", "[Ljava/lang/String;" },
-		{ nullptr, "V" }
-	};
-
-	int idx = 0;
-
-	while (_type_to_vtype[idx].name) {
-		if (p_type == _type_to_vtype[idx].name) {
-			return _type_to_vtype[idx].sig;
-		}
-
-		idx++;
-	}
-
-	return "L" + p_type.replace(".", "/") + ";";
-}

+ 46 - 2
platform/android/jni_utils.h

@@ -31,9 +31,10 @@
 #ifndef JNI_UTILS_H
 #define JNI_UTILS_H
 
-#include "string_android.h"
+#include "thread_jandroid.h"
 
 #include "core/config/engine.h"
+#include "core/string/ustring.h"
 #include "core/variant/variant.h"
 
 #include <jni.h>
@@ -52,6 +53,49 @@ Variant _jobject_to_variant(JNIEnv *env, jobject obj);
 
 Variant::Type get_jni_type(const String &p_type);
 
-String get_jni_sig(const String &p_type);
+/**
+ * Convert a Godot Callable to a org.godotengine.godot.variant.Callable java object.
+ * @param p_env JNI environment instance
+ * @param p_callable Callable parameter to convert. If null or invalid type, a null jobject is returned.
+ * @return org.godotengine.godot.variant.Callable jobject or null
+ */
+jobject callable_to_jcallable(JNIEnv *p_env, const Variant &p_callable);
+
+/**
+ * Convert a org.godotengine.godot.variant.Callable java object to a Godot Callable variant.
+ * @param p_env JNI environment instance
+ * @param p_jcallable_obj org.godotengine.godot.variant.Callable java object to convert.
+ * @return Callable variant
+ */
+Callable jcallable_to_callable(JNIEnv *p_env, jobject p_jcallable_obj);
+
+/**
+ * Converts a java.lang.CharSequence object to a Godot String.
+ * @param p_env  JNI environment instance
+ * @param p_charsequence java.lang.CharSequence object to convert
+ * @return Godot String instance.
+ */
+String charsequence_to_string(JNIEnv *p_env, jobject p_charsequence);
+
+/**
+ * Converts JNI jstring to Godot String.
+ * @param source Source JNI string. If null an empty string is returned.
+ * @param env JNI environment instance. If null obtained by get_jni_env().
+ * @return Godot string instance.
+ */
+static inline String jstring_to_string(jstring source, JNIEnv *env = nullptr) {
+	String result;
+	if (source) {
+		if (!env) {
+			env = get_jni_env();
+		}
+		const char *const source_utf8 = env->GetStringUTFChars(source, nullptr);
+		if (source_utf8) {
+			result.parse_utf8(source_utf8);
+			env->ReleaseStringUTFChars(source, source_utf8);
+		}
+	}
+	return result;
+}
 
 #endif // JNI_UTILS_H

+ 5 - 1
platform/android/plugin/godot_plugin_jni.cpp

@@ -33,7 +33,6 @@
 #include "api/java_class_wrapper.h"
 #include "api/jni_singleton.h"
 #include "jni_utils.h"
-#include "string_android.h"
 
 #include "core/config/engine.h"
 #include "core/error/error_macros.h"
@@ -136,5 +135,10 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitS
 	}
 
 	singleton->emit_signalp(StringName(signal_name), args, count);
+
+	// Manually invoke the destructor to decrease the reference counts for the variant arguments.
+	for (int i = 0; i < count; i++) {
+		variant_params[i].~Variant();
+	}
 }
 }

+ 0 - 1
platform/android/tts_android.cpp

@@ -32,7 +32,6 @@
 
 #include "java_godot_wrapper.h"
 #include "os_android.h"
-#include "string_android.h"
 #include "thread_jandroid.h"
 
 bool TTS_Android::initialized = false;

+ 130 - 0
platform/android/variant/callable_jni.cpp

@@ -0,0 +1,130 @@
+/**************************************************************************/
+/*  callable_jni.cpp                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* 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.                 */
+/**************************************************************************/
+
+#include "callable_jni.h"
+
+#include "jni_utils.h"
+
+#include "core/error/error_macros.h"
+#include "core/object/object.h"
+
+static Callable _generate_callable(JNIEnv *p_env, jlong p_object_id, jstring p_method_name, jobjectArray p_parameters) {
+	Object *obj = ObjectDB::get_instance(ObjectID(p_object_id));
+	ERR_FAIL_NULL_V(obj, Callable());
+
+	String str_method = jstring_to_string(p_method_name, p_env);
+
+	int count = p_env->GetArrayLength(p_parameters);
+
+	Variant *args = (Variant *)alloca(sizeof(Variant) * count);
+	const Variant **argptrs = (const Variant **)alloca(sizeof(Variant *) * count);
+
+	for (int i = 0; i < count; i++) {
+		jobject jobj = p_env->GetObjectArrayElement(p_parameters, i);
+		ERR_FAIL_NULL_V(jobj, Callable());
+		memnew_placement(&args[i], Variant(_jobject_to_variant(p_env, jobj)));
+		argptrs[i] = &args[i];
+		p_env->DeleteLocalRef(jobj);
+	}
+
+	Callable ret = Callable(obj, str_method).bindp(argptrs, count);
+
+	// Manually invoke the destructor to decrease the reference counts for the variant arguments.
+	for (int i = 0; i < count; i++) {
+		args[i].~Variant();
+	}
+
+	return ret;
+}
+
+extern "C" {
+JNIEXPORT jobject JNICALL Java_org_godotengine_godot_variant_Callable_nativeCall(JNIEnv *p_env, jclass p_clazz, jlong p_native_callable, jobjectArray p_parameters) {
+	const Variant *callable_variant = reinterpret_cast<const Variant *>(p_native_callable);
+	ERR_FAIL_NULL_V(callable_variant, nullptr);
+	if (callable_variant->get_type() != Variant::CALLABLE) {
+		return nullptr;
+	}
+
+	int count = p_env->GetArrayLength(p_parameters);
+
+	Variant *args = (Variant *)alloca(sizeof(Variant) * count);
+	const Variant **argptrs = (const Variant **)alloca(sizeof(Variant *) * count);
+
+	for (int i = 0; i < count; i++) {
+		jobject jobj = p_env->GetObjectArrayElement(p_parameters, i);
+		ERR_FAIL_NULL_V(jobj, nullptr);
+		memnew_placement(&args[i], Variant(_jobject_to_variant(p_env, jobj)));
+		argptrs[i] = &args[i];
+		p_env->DeleteLocalRef(jobj);
+	}
+
+	Callable callable = *callable_variant;
+	jobject ret = nullptr;
+	if (callable.is_valid()) {
+		Callable::CallError err;
+		Variant result;
+		callable.callp(argptrs, count, result, err);
+		jvalret jresult = _variant_to_jvalue(p_env, result.get_type(), &result, true);
+		ret = jresult.obj;
+	}
+
+	// Manually invoke the destructor to decrease the reference counts for the variant arguments.
+	for (int i = 0; i < count; i++) {
+		args[i].~Variant();
+	}
+
+	return ret;
+}
+
+JNIEXPORT jobject JNICALL Java_org_godotengine_godot_variant_Callable_nativeCallObject(JNIEnv *p_env, jclass p_clazz, jlong p_object_id, jstring p_method_name, jobjectArray p_parameters) {
+	Callable callable = _generate_callable(p_env, p_object_id, p_method_name, p_parameters);
+	if (callable.is_valid()) {
+		Variant result = callable.call();
+		jvalret jresult = _variant_to_jvalue(p_env, result.get_type(), &result, true);
+		return jresult.obj;
+	} else {
+		return nullptr;
+	}
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_variant_Callable_nativeCallObjectDeferred(JNIEnv *p_env, jclass p_clazz, jlong p_object_id, jstring p_method_name, jobjectArray p_parameters) {
+	Callable callable = _generate_callable(p_env, p_object_id, p_method_name, p_parameters);
+	if (callable.is_valid()) {
+		callable.call_deferred();
+	}
+}
+
+JNIEXPORT void JNICALL
+Java_org_godotengine_godot_variant_Callable_releaseNativePointer(JNIEnv *p_env, jclass clazz, jlong p_native_pointer) {
+	Variant *variant = reinterpret_cast<Variant *>(p_native_pointer);
+	ERR_FAIL_NULL(variant);
+	memdelete(variant);
+}
+}

+ 9 - 27
platform/android/string_android.h → platform/android/variant/callable_jni.h

@@ -1,5 +1,5 @@
 /**************************************************************************/
-/*  string_android.h                                                      */
+/*  callable_jni.h                                                        */
 /**************************************************************************/
 /*                         This file is part of:                          */
 /*                             GODOT ENGINE                               */
@@ -28,34 +28,16 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
 /**************************************************************************/
 
-#ifndef STRING_ANDROID_H
-#define STRING_ANDROID_H
-
-#include "thread_jandroid.h"
-
-#include "core/string/ustring.h"
+#ifndef CALLABLE_JNI_H
+#define CALLABLE_JNI_H
 
 #include <jni.h>
 
-/**
- * Converts JNI jstring to Godot String.
- * @param source Source JNI string. If null an empty string is returned.
- * @param env JNI environment instance. If null obtained by get_jni_env().
- * @return Godot string instance.
- */
-static inline String jstring_to_string(jstring source, JNIEnv *env = nullptr) {
-	String result;
-	if (source) {
-		if (!env) {
-			env = get_jni_env();
-		}
-		const char *const source_utf8 = env->GetStringUTFChars(source, nullptr);
-		if (source_utf8) {
-			result.parse_utf8(source_utf8);
-			env->ReleaseStringUTFChars(source, source_utf8);
-		}
-	}
-	return result;
+extern "C" {
+JNIEXPORT jobject JNICALL Java_org_godotengine_godot_variant_Callable_nativeCall(JNIEnv *p_env, jclass p_clazz, jlong p_native_callable, jobjectArray p_parameters);
+JNIEXPORT jobject JNICALL Java_org_godotengine_godot_variant_Callable_nativeCallObject(JNIEnv *p_env, jclass p_clazz, jlong p_object_id, jstring p_method_name, jobjectArray p_parameters);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_variant_Callable_nativeCallObjectDeferred(JNIEnv *p_env, jclass p_clazz, jlong p_object_id, jstring p_method_name, jobjectArray p_parameters);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_variant_Callable_releaseNativePointer(JNIEnv *p_env, jclass clazz, jlong p_native_pointer);
 }
 
-#endif // STRING_ANDROID_H
+#endif // CALLABLE_JNI_H