diff --git a/doc/classes/JavaClassWrapper.xml b/doc/classes/JavaClassWrapper.xml index c6f76700b3..9bc455280c 100644 --- a/doc/classes/JavaClassWrapper.xml +++ b/doc/classes/JavaClassWrapper.xml @@ -21,6 +21,42 @@ $DOCS_URL/tutorials/platform/android/javaclasswrapper_and_androidruntimeplugin.html + + + + + + Creates a [JavaObject] implementing the given Java interfaces using the given [Object] as the implementation. + The [param object] must contain methods signatures matching the methods signatures from the passed Java [param interfaces]. Invoking methods from the Java [param interfaces] will route to the matching [param object] method. + [codeblock] + class PrintProxy: + func println(content: String) -> void: + print(content) + + var print_proxy = PrintProxy.new() + var printer_object = JavaClassWrapper.create_proxy(print_proxy, ["android.util.Printer"]) + printer_object.println("Hello Godot World!") + [/codeblock] + [b]Note:[/b] This method only works on Android. On every other platform, this method will always return [code]null[/code]. + + + + + + + + Creates a [JavaObject] implementing the Java Single Abstract Method (SAM) interface using the Godot [Callable] as the implementation. + The [param sam_interface] [b]must be[/b] a Java SAM interface, meaning it must only have a single abstract method to implement. + The [param callable] must be able to handle the same parameter types as the SAM interface method, and must provide the same return type. The [param callable] will be invoked as a callback, passing the arguments from the Java SAM interface method. + [codeblock] + var cb = func (content: String) -> void: + print(content) + var callback = JavaClassWrapper.create_sam_callback("android.util.Printer", cb) + callback.println("Hello Godot World!") + [/codeblock] + [b]Note:[/b] This method only works on Android. On every other platform, this method will always return [code]null[/code]. + + diff --git a/platform/android/api/api.cpp b/platform/android/api/api.cpp index 24d67bdbe6..5a10a47ba5 100644 --- a/platform/android/api/api.cpp +++ b/platform/android/api/api.cpp @@ -74,6 +74,8 @@ void JavaObject::_bind_methods() { void JavaClassWrapper::_bind_methods() { ClassDB::bind_method(D_METHOD("wrap", "name"), &JavaClassWrapper::wrap); ClassDB::bind_method(D_METHOD("get_exception"), &JavaClassWrapper::get_exception); + ClassDB::bind_method(D_METHOD("create_sam_callback", "sam_interface", "callable"), &JavaClassWrapper::create_sam_callback); + ClassDB::bind_method(D_METHOD("create_proxy", "object", "interfaces"), &JavaClassWrapper::create_proxy); } #if !defined(ANDROID_ENABLED) @@ -125,6 +127,14 @@ Ref JavaClassWrapper::_wrap(const String &, bool) { return Ref(); } +Ref JavaClassWrapper::create_sam_callback(const String &p_interface, const Callable &p_callable) { + return Ref(); +} + +Ref JavaClassWrapper::create_proxy(const Object *p_object, const PackedStringArray &p_interfaces) { + return Ref(); +} + JavaClassWrapper::JavaClassWrapper() { singleton = this; } diff --git a/platform/android/api/java_class_wrapper.h b/platform/android/api/java_class_wrapper.h index ab469217c9..9e5657fb87 100644 --- a/platform/android/api/java_class_wrapper.h +++ b/platform/android/api/java_class_wrapper.h @@ -191,6 +191,7 @@ class JavaClass : public RefCounted { String java_constructor_name; HashMap> methods; jclass _class; + bool is_interface; #endif protected: @@ -252,8 +253,10 @@ class JavaClassWrapper : public Object { jmethodID Class_getConstructors; jmethodID Class_getDeclaredMethods; jmethodID Class_getFields; + jmethodID Class_getInterfaces; jmethodID Class_getName; jmethodID Class_getSuperclass; + jmethodID Class_isInterface; jmethodID Constructor_getParameterTypes; jmethodID Constructor_getModifiers; jmethodID Method_getParameterTypes; @@ -272,7 +275,16 @@ class JavaClassWrapper : public Object { jmethodID Float_floatValue; jmethodID Double_doubleValue; + jclass proxy_class; + jmethodID Proxy_isProxyClass; + + jclass android_runtime_class; + jmethodID ARP_create_proxy_from_godot_callable; + jmethodID ARP_create_proxy_from_godot_object_id; + + bool _is_proxy_class(JNIEnv *env, jclass p_class); bool _get_type_sig(JNIEnv *env, jobject obj, uint32_t &sig, String &strsig); + bool _wrap_class_components(JNIEnv *p_env, const Ref &p_java_class, jclass p_class, bool p_allow_non_public_methods_access); #endif Ref exception; @@ -291,6 +303,9 @@ public: return _wrap(p_class, false); } + Ref create_sam_callback(const String &p_sam_interface, const Callable &p_callable); + Ref create_proxy(const Object *p_object, const PackedStringArray &p_interfaces); + Ref get_exception() { return exception; } diff --git a/platform/android/java/app/src/instrumented/assets/.godot/global_script_class_cache.cfg b/platform/android/java/app/src/instrumented/assets/.godot/global_script_class_cache.cfg index b6ed5489bf..1f8f066232 100644 --- a/platform/android/java/app/src/instrumented/assets/.godot/global_script_class_cache.cfg +++ b/platform/android/java/app/src/instrumented/assets/.godot/global_script_class_cache.cfg @@ -1,12 +1,4 @@ list=[{ -"base": &"RefCounted", -"class": &"BaseTest", -"icon": "", -"is_abstract": true, -"is_tool": false, -"language": &"GDScript", -"path": "res://test/base_test.gd" -}, { "base": &"BaseTest", "class": &"FileAccessTests", "icon": "", @@ -22,4 +14,12 @@ list=[{ "is_tool": false, "language": &"GDScript", "path": "res://test/javaclasswrapper/java_class_wrapper_tests.gd" +}, { +"base": &"RefCounted", +"class": &"BaseTest", +"icon": "", +"is_abstract": true, +"is_tool": false, +"language": &"GDScript", +"path": "res://test/base_test.gd" }] diff --git a/platform/android/java/app/src/instrumented/assets/main.tscn b/platform/android/java/app/src/instrumented/assets/main.tscn index 848845bab6..692618a73f 100644 --- a/platform/android/java/app/src/instrumented/assets/main.tscn +++ b/platform/android/java/app/src/instrumented/assets/main.tscn @@ -1,29 +1,29 @@ -[gd_scene load_steps=2 format=3 uid="uid://cg3hylang5fxn"] +[gd_scene format=3 uid="uid://cg3hylang5fxn"] [ext_resource type="Script" uid="uid://bv6y7in6otgcm" path="res://main.gd" id="1_j0gfq"] -[node name="Main" type="Node2D"] +[node name="Main" type="Node2D" unique_id=852911723] script = ExtResource("1_j0gfq") -[node name="VBoxContainer" type="VBoxContainer" parent="."] +[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1839352715] offset_left = 68.0 offset_top = 102.0 offset_right = 506.0 offset_bottom = 408.0 theme_override_constants/separation = 25 -[node name="PluginToastButton" type="Button" parent="VBoxContainer"] +[node name="PluginToastButton" type="Button" parent="VBoxContainer" unique_id=1670434164] custom_minimum_size = Vector2(0, 50) layout_mode = 2 text = "Plugin Toast " -[node name="VibrationButton" type="Button" parent="VBoxContainer"] +[node name="VibrationButton" type="Button" parent="VBoxContainer" unique_id=648980813] custom_minimum_size = Vector2(0, 50) layout_mode = 2 text = "Vibration" -[node name="GDScriptToastButton" type="Button" parent="VBoxContainer"] +[node name="GDScriptToastButton" type="Button" parent="VBoxContainer" unique_id=95554078] custom_minimum_size = Vector2(0, 50) layout_mode = 2 text = "GDScript Toast diff --git a/platform/android/java/app/src/instrumented/assets/project.godot b/platform/android/java/app/src/instrumented/assets/project.godot index f4ec299792..68bc653c43 100644 --- a/platform/android/java/app/src/instrumented/assets/project.godot +++ b/platform/android/java/app/src/instrumented/assets/project.godot @@ -8,11 +8,15 @@ config_version=5 +[animation] + +compatibility/default_parent_skeleton_in_mesh_instance_3d=true + [application] config/name="Godot App Instrumentation Tests" run/main_scene="res://main.tscn" -config/features=PackedStringArray("4.5", "GL Compatibility") +config/features=PackedStringArray("4.6", "GL Compatibility") config/icon="res://icon.svg" [debug] diff --git a/platform/android/java/app/src/instrumented/assets/test/javaclasswrapper/java_class_wrapper_tests.gd b/platform/android/java/app/src/instrumented/assets/test/javaclasswrapper/java_class_wrapper_tests.gd index 65843f1f3b..1d4a57669c 100644 --- a/platform/android/java/app/src/instrumented/assets/test/javaclasswrapper/java_class_wrapper_tests.gd +++ b/platform/android/java/app/src/instrumented/assets/test/javaclasswrapper/java_class_wrapper_tests.gd @@ -20,6 +20,10 @@ func run_tests(): __exec_test(test_callable) + __exec_test(test_interface_callable_proxy) + + __exec_test(test_interface_object_proxy) + print("JavaClassWrapper tests finished.") print("Tests started: " + str(_test_started)) print("Tests completed: " + str(_test_completed)) @@ -169,3 +173,34 @@ func test_callable() -> bool: assert_equal(cb1_data['called'], true) return true + +func test_interface_callable_proxy() -> bool: + var cb1_data := {called = false, content = ""} + var cb1 = func (content: String) -> void: + cb1_data['called'] = true + cb1_data['content'] = content + + var printer_proxy = JavaClassWrapper.create_sam_callback("android.util.Printer", cb1) + assert_true(printer_proxy != null) + + printer_proxy.println("This is a callback test") + assert_equal(cb1_data['called'], true) + assert_equal(cb1_data['content'], "This is a callback test") + return true + +class PrintProxy: + var test_data := {called = false, content = ""} + + func println(content: String) -> void: + test_data['called'] = true + test_data['content'] = content + +func test_interface_object_proxy() -> bool: + var print_object = PrintProxy.new() + var proxy = JavaClassWrapper.create_proxy(print_object, ["android.util.Printer"]) + assert_true(proxy != null) + + proxy.println("This is proxy test") + assert_equal(print_object.test_data['called'], true) + assert_equal(print_object.test_data['content'], "This is proxy test") + return true diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt index 4394d51404..19c9d51b1e 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/plugin/AndroidRuntimePlugin.kt @@ -32,10 +32,13 @@ package org.godotengine.godot.plugin import android.content.Intent import android.util.Log +import androidx.annotation.Keep import androidx.core.net.toUri import org.godotengine.godot.Godot import org.godotengine.godot.variant.Callable +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Proxy /** * Built-in Godot Android plugin used to provide access to the Android runtime capabilities. @@ -43,7 +46,76 @@ import org.godotengine.godot.variant.Callable * @see Integrating with Android APIs */ class AndroidRuntimePlugin(godot: Godot) : GodotPlugin(godot) { - private val TAG = AndroidRuntimePlugin::class.java.simpleName + + companion object { + private val TAG = AndroidRuntimePlugin::class.java.simpleName + + /** + * Helper method used to generate Godot Proxy instances. + */ + @JvmStatic + @Keep + private fun generateProxyInstance(interfaces: Array, invocationHandler: InvocationHandler): Any? { + try { + val interfaceClasses = interfaces.map { Class.forName(it) }.toTypedArray() + + val proxy = Proxy.newProxyInstance(invocationHandler.javaClass.classLoader, interfaceClasses, invocationHandler) + return proxy + } catch (e: Exception) { + Log.w(TAG, "Error generating Godot proxy for interfaces ${interfaces.joinToString(",")}", e) + } + return null + } + + /** + * Utility method used to create [java.lang.reflect.Proxy] instance wrapping a given Godot [Callable]. + * + * The [Proxy] instance is used to implement one SAM interface with the [Callable] serving as the delegate + * implementation for the SAM interface overridden methods. + */ + @JvmStatic + @Keep + private fun createProxyFromGodotCallable(interfaceName: String, godotCallable: Callable): Any? { + return generateProxyInstance(arrayOf(interfaceName)) { proxy, method, args -> + when (method.name) { + // We automatically handle 'toString', 'equals' and 'hashCode' to simplify the task of the caller + // and provide consistency. + "toString" -> "Godot Callable Proxy for $interfaceName" + "equals" -> proxy == args[0] + "hashCode" -> godotCallable.hashCode() + + // Invocation for the interface single abstract method falls here and is dispatched to the + // Godot [Callable]. + else -> godotCallable.call(*args) + } + } + } + + /** + * Utility method used to create [java.lang.reflect.Proxy] instance wrapping a given Godot Object represented by + * its ObjectID. + * + * The [Proxy] instance is used to implement one or multiple interfaces with the Object represented by + * [godotObjectID] serving as the delegate implementation for the interface(s) overridden methods. + */ + @JvmStatic + @Keep + private fun createProxyFromGodotObjectID(godotObjectID: Long, interfaces: Array): Any? { + return generateProxyInstance(interfaces) { proxy, method, args -> + when (val methodName = method.name) { + // We automatically handle 'toString', 'equals' and 'hashCode' to simplify the task of the caller + // and provide consistency. + "toString" -> "Godot Object Proxy for ${interfaces.joinToString(",")}" + "equals" -> proxy == args[0] + "hashCode" -> godotObjectID + + // Invocation for the remaining interface(s) methods falls here and is dispatched to the + // Godot Object. + else -> Callable.call(godotObjectID, methodName, *args) + } + } + } + } override fun getPluginName() = "AndroidRuntime" diff --git a/platform/android/java_class_wrapper.cpp b/platform/android/java_class_wrapper.cpp index 051775f043..f1a9b689df 100644 --- a/platform/android/java_class_wrapper.cpp +++ b/platform/android/java_class_wrapper.cpp @@ -1461,74 +1461,58 @@ bool JavaClass::_convert_object_to_variant(JNIEnv *env, jobject obj, Variant &va return false; } -Ref JavaClassWrapper::_wrap(const String &p_class, bool p_allow_non_public_methods_access) { - String class_name_dots = p_class.replace_char('/', '.'); - if (class_cache.has(class_name_dots)) { - return class_cache[class_name_dots]; +bool JavaClassWrapper::_wrap_class_components(JNIEnv *p_env, const Ref &p_java_class, jclass p_class, bool p_allow_non_public_methods_access) { + ERR_FAIL_NULL_V(p_class, false); + + jobjectArray constructors = (jobjectArray)p_env->CallObjectMethod(p_class, Class_getConstructors); + if (p_env->ExceptionCheck()) { + p_env->ExceptionDescribe(); + p_env->ExceptionClear(); } + ERR_FAIL_NULL_V(constructors, false); - JNIEnv *env = get_jni_env(); - ERR_FAIL_NULL_V(env, Ref()); - - jclass bclass = jni_find_class(env, class_name_dots.replace_char('.', '/').utf8().get_data()); - ERR_FAIL_NULL_V_MSG(bclass, Ref(), vformat("Java class '%s' not found.", p_class)); - - jobjectArray constructors = (jobjectArray)env->CallObjectMethod(bclass, Class_getConstructors); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->ExceptionClear(); + jobjectArray methods = (jobjectArray)p_env->CallObjectMethod(p_class, Class_getDeclaredMethods); + if (p_env->ExceptionCheck()) { + p_env->ExceptionDescribe(); + p_env->ExceptionClear(); } - ERR_FAIL_NULL_V(constructors, Ref()); + ERR_FAIL_NULL_V(methods, false); - jobjectArray methods = (jobjectArray)env->CallObjectMethod(bclass, Class_getDeclaredMethods); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - env->ExceptionClear(); - } - ERR_FAIL_NULL_V(methods, Ref()); - - Ref java_class = memnew(JavaClass); - java_class->java_class_name = class_name_dots; - Vector class_name_parts = class_name_dots.split("."); - java_class->java_constructor_name = class_name_parts[class_name_parts.size() - 1]; - java_class->_class = (jclass)env->NewGlobalRef(bclass); - class_cache[class_name_dots] = java_class; - - int constructor_count = env->GetArrayLength(constructors); - int method_count = env->GetArrayLength(methods); + int constructor_count = p_env->GetArrayLength(constructors); + int method_count = p_env->GetArrayLength(methods); int methods_and_constructors_count = method_count + constructor_count; for (int i = 0; i < methods_and_constructors_count; i++) { bool is_constructor = i < constructor_count; jobject obj = is_constructor - ? env->GetObjectArrayElement(constructors, i) - : env->GetObjectArrayElement(methods, i - constructor_count); + ? p_env->GetObjectArrayElement(constructors, i) + : p_env->GetObjectArrayElement(methods, i - constructor_count); ERR_CONTINUE(!obj); String str_method; if (is_constructor) { str_method = ""; } else { - jstring name = (jstring)env->CallObjectMethod(obj, Method_getName); - str_method = jstring_to_string(name, env); - env->DeleteLocalRef(name); + jstring name = (jstring)p_env->CallObjectMethod(obj, Method_getName); + str_method = jstring_to_string(name, p_env); + p_env->DeleteLocalRef(name); } Vector params; - jint mods = env->CallIntMethod(obj, is_constructor ? Constructor_getModifiers : Method_getModifiers); + jint mods = p_env->CallIntMethod(obj, is_constructor ? Constructor_getModifiers : Method_getModifiers); bool is_public = (mods & 0x0001) != 0; // java.lang.reflect.Modifier.PUBLIC if (!is_public && (is_constructor || !p_allow_non_public_methods_access)) { - env->DeleteLocalRef(obj); + p_env->DeleteLocalRef(obj); continue; //not public bye } - jobjectArray param_types = (jobjectArray)env->CallObjectMethod(obj, is_constructor ? Constructor_getParameterTypes : Method_getParameterTypes); - int count = env->GetArrayLength(param_types); + jobjectArray param_types = (jobjectArray)p_env->CallObjectMethod(obj, is_constructor ? Constructor_getParameterTypes : Method_getParameterTypes); + int count = p_env->GetArrayLength(param_types); - if (!java_class->methods.has(str_method)) { - java_class->methods[str_method] = List(); + if (!p_java_class->methods.has(str_method)) { + p_java_class->methods[str_method] = List(); } JavaClass::MethodInfo mi; @@ -1539,24 +1523,24 @@ Ref JavaClassWrapper::_wrap(const String &p_class, bool p_allow_non_p String signature = "("; for (int j = 0; j < count; j++) { - jobject obj2 = env->GetObjectArrayElement(param_types, j); + jobject obj2 = p_env->GetObjectArrayElement(param_types, j); String strsig; uint32_t sig = 0; - if (!_get_type_sig(env, obj2, sig, strsig)) { + if (!_get_type_sig(p_env, obj2, sig, strsig)) { valid = false; - env->DeleteLocalRef(obj2); + p_env->DeleteLocalRef(obj2); break; } signature += strsig; mi.param_types.push_back(sig); mi.param_sigs.push_back(strsig); - env->DeleteLocalRef(obj2); + p_env->DeleteLocalRef(obj2); } if (!valid) { - print_line("Method can't be bound (unsupported arguments): " + class_name_dots + "::" + str_method); - env->DeleteLocalRef(obj); - env->DeleteLocalRef(param_types); + print_line("Method can't be bound (unsupported arguments): " + p_java_class->java_class_name + "::" + str_method); + p_env->DeleteLocalRef(obj); + p_env->DeleteLocalRef(param_types); continue; } @@ -1566,27 +1550,27 @@ Ref JavaClassWrapper::_wrap(const String &p_class, bool p_allow_non_p signature += "V"; mi.return_type = JavaClass::ARG_TYPE_CLASS; } else { - jobject return_type = (jobject)env->CallObjectMethod(obj, Method_getReturnType); + jobject return_type = (jobject)p_env->CallObjectMethod(obj, Method_getReturnType); String strsig; uint32_t sig = 0; - if (!_get_type_sig(env, return_type, sig, strsig)) { - print_line("Method can't be bound (unsupported return type): " + class_name_dots + "::" + str_method); - env->DeleteLocalRef(obj); - env->DeleteLocalRef(param_types); - env->DeleteLocalRef(return_type); + if (!_get_type_sig(p_env, return_type, sig, strsig)) { + print_line("Method can't be bound (unsupported return type): " + p_java_class->java_class_name + "::" + str_method); + p_env->DeleteLocalRef(obj); + p_env->DeleteLocalRef(param_types); + p_env->DeleteLocalRef(return_type); continue; } signature += strsig; mi.return_type = sig; - env->DeleteLocalRef(return_type); + p_env->DeleteLocalRef(return_type); } bool discard = false; - for (List::Element *E = java_class->methods[str_method].front(); E; E = E->next()) { + for (List::Element *E = p_java_class->methods[str_method].front(); E; E = E->next()) { float new_likeliness = 0; float existing_likeliness = 0; @@ -1617,7 +1601,7 @@ Ref JavaClassWrapper::_wrap(const String &p_class, bool p_allow_non_p } if (new_likeliness > existing_likeliness) { - java_class->methods[str_method].erase(E); + p_java_class->methods[str_method].erase(E); break; } else { discard = true; @@ -1626,72 +1610,202 @@ Ref JavaClassWrapper::_wrap(const String &p_class, bool p_allow_non_p if (!discard) { if (mi._static) { - mi.method = env->GetStaticMethodID(bclass, str_method.utf8().get_data(), signature.utf8().get_data()); + mi.method = p_env->GetStaticMethodID(p_class, str_method.utf8().get_data(), signature.utf8().get_data()); } else { - mi.method = env->GetMethodID(bclass, str_method.utf8().get_data(), signature.utf8().get_data()); + mi.method = p_env->GetMethodID(p_class, str_method.utf8().get_data(), signature.utf8().get_data()); } - if (env->ExceptionCheck()) { + if (p_env->ExceptionCheck()) { // Exceptions may be thrown when trying to access hidden methods; write the exception to the logs and continue. - env->ExceptionDescribe(); - env->ExceptionClear(); + p_env->ExceptionDescribe(); + p_env->ExceptionClear(); continue; } if (mi.method) { - java_class->methods[str_method].push_back(mi); + p_java_class->methods[str_method].push_back(mi); } } - env->DeleteLocalRef(obj); - env->DeleteLocalRef(param_types); + p_env->DeleteLocalRef(obj); + p_env->DeleteLocalRef(param_types); } - env->DeleteLocalRef(constructors); - env->DeleteLocalRef(methods); + p_env->DeleteLocalRef(constructors); + p_env->DeleteLocalRef(methods); - jobjectArray fields = (jobjectArray)env->CallObjectMethod(bclass, Class_getFields); + jobjectArray fields = (jobjectArray)p_env->CallObjectMethod(p_class, Class_getFields); - int count = env->GetArrayLength(fields); + int count = p_env->GetArrayLength(fields); for (int i = 0; i < count; i++) { - jobject obj = env->GetObjectArrayElement(fields, i); + jobject obj = p_env->GetObjectArrayElement(fields, i); ERR_CONTINUE(!obj); - jstring name = (jstring)env->CallObjectMethod(obj, Field_getName); - String str_field = jstring_to_string(name, env); - env->DeleteLocalRef(name); - int mods = env->CallIntMethod(obj, Field_getModifiers); + jstring name = (jstring)p_env->CallObjectMethod(obj, Field_getName); + String str_field = jstring_to_string(name, p_env); + p_env->DeleteLocalRef(name); + int mods = p_env->CallIntMethod(obj, Field_getModifiers); if ((mods & 0x8) && (mods & 0x1)) { //static public! - jobject objc = env->CallObjectMethod(obj, Field_get, nullptr); + jobject objc = p_env->CallObjectMethod(obj, Field_get, nullptr); if (objc) { uint32_t sig; String strsig; - jclass cl = env->GetObjectClass(objc); - if (JavaClassWrapper::_get_type_sig(env, cl, sig, strsig)) { + jclass cl = p_env->GetObjectClass(objc); + if (JavaClassWrapper::_get_type_sig(p_env, cl, sig, strsig)) { if ((sig & JavaClass::ARG_TYPE_MASK) <= JavaClass::ARG_TYPE_STRING) { Variant value; - if (JavaClass::_convert_object_to_variant(env, objc, value, sig)) { - java_class->constant_map[str_field] = value; + if (JavaClass::_convert_object_to_variant(p_env, objc, value, sig)) { + p_java_class->constant_map[str_field] = value; } } } - env->DeleteLocalRef(cl); + p_env->DeleteLocalRef(cl); } - env->DeleteLocalRef(objc); + p_env->DeleteLocalRef(objc); } - env->DeleteLocalRef(obj); + p_env->DeleteLocalRef(obj); } - env->DeleteLocalRef(fields); + p_env->DeleteLocalRef(fields); + return true; +} + +Ref JavaClassWrapper::_wrap(const String &p_class, bool p_allow_non_public_methods_access) { + String class_name_dots = p_class.replace_char('/', '.'); + if (class_cache.has(class_name_dots)) { + return class_cache[class_name_dots]; + } + + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, Ref()); + + jclass bclass = jni_find_class(env, class_name_dots.replace_char('.', '/').utf8().get_data()); + ERR_FAIL_NULL_V_MSG(bclass, Ref(), vformat("Java class '%s' not found.", p_class)); + + Ref java_class; + java_class.instantiate(); + java_class->java_class_name = class_name_dots; + Vector class_name_parts = class_name_dots.split("."); + java_class->java_constructor_name = class_name_parts[class_name_parts.size() - 1]; + java_class->_class = (jclass)env->NewGlobalRef(bclass); + java_class->is_interface = env->CallBooleanMethod(bclass, Class_isInterface); + + bool class_components_configured; + if (_is_proxy_class(env, bclass)) { + // Proxy class components must be setup using the interfaces they implement. + jobjectArray interfaces = (jobjectArray)env->CallObjectMethod(bclass, Class_getInterfaces); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + int interfaces_count = interfaces == nullptr ? 0 : env->GetArrayLength(interfaces); + for (int i = 0; i < interfaces_count; i++) { + jclass interface = (jclass)env->GetObjectArrayElement(interfaces, i); + ERR_CONTINUE(!interface); + + jstring j_interface_name = (jstring)env->CallObjectMethod(interface, Class_getName); + String interface_name = jstring_to_string(j_interface_name, env); + env->DeleteLocalRef(j_interface_name); + if (!_wrap_class_components(env, java_class, interface, p_allow_non_public_methods_access)) { + ERR_PRINT(vformat("Unable to set up components for proxy class interface %s.", interface_name)); + continue; + } + + class_components_configured = true; + } + } else { + class_components_configured = _wrap_class_components(env, java_class, bclass, p_allow_non_public_methods_access); + } env->DeleteLocalRef(bclass); + if (!class_components_configured) { + java_class.unref(); + return Ref(); + } + + // Cache the initialized JavaClass instance. + class_cache[class_name_dots] = java_class; + return java_class; } +Ref JavaClassWrapper::create_sam_callback(const String &p_sam_interface, const Callable &p_callable) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, Ref()); + ERR_FAIL_NULL_V(android_runtime_class, Ref()); + ERR_FAIL_NULL_V(ARP_create_proxy_from_godot_callable, Ref()); + + Ref interface_class = wrap(p_sam_interface); + if (interface_class.is_null()) { + ERR_PRINT(vformat("Invalid java class: %s.", p_sam_interface)); + return Ref(); + } + if (!interface_class->is_interface) { + ERR_PRINT(vformat("Java class %s must be an interface.", p_sam_interface)); + return Ref(); + } + + if (interface_class->methods.size() != 1) { + ERR_PRINT(vformat("%s must be a Single Abstract Method (SAM) interface.", p_sam_interface)); + return Ref(); + } + + jobject j_callable = callable_to_jcallable(env, p_callable); + jstring j_interface = env->NewStringUTF(p_sam_interface.utf8().get_data()); + + jobject proxy = env->CallStaticObjectMethod(android_runtime_class, ARP_create_proxy_from_godot_callable, j_interface, j_callable); + Ref result = _jobject_to_variant(env, proxy); + + env->DeleteLocalRef(j_callable); + env->DeleteLocalRef(j_interface); + env->DeleteLocalRef(proxy); + + return result; +} + +Ref JavaClassWrapper::create_proxy(const Object *p_object, const PackedStringArray &p_interfaces) { + ERR_FAIL_NULL_V(p_object, Ref()); + ERR_FAIL_COND_V(p_interfaces.is_empty(), Ref()); + + JNIEnv *env = get_jni_env(); + ERR_FAIL_NULL_V(env, Ref()); + ERR_FAIL_NULL_V(android_runtime_class, Ref()); + ERR_FAIL_NULL_V(ARP_create_proxy_from_godot_callable, Ref()); + + for (const String &interface_name : p_interfaces) { + Ref interface_class = wrap(interface_name); + if (interface_class.is_null()) { + ERR_PRINT(vformat("Invalid java class: %s.", interface_name)); + return Ref(); + } + + if (!interface_class->is_interface) { + ERR_PRINT(vformat("Java class %s must be an interface.", interface_name)); + return Ref(); + } + } + + jlong object_id = p_object->get_instance_id(); + jobjectArray j_interfaces = env->NewObjectArray(p_interfaces.size(), jni_find_class(env, "java/lang/String"), nullptr); + for (int i = 0; i < p_interfaces.size(); i++) { + jstring j_interface = env->NewStringUTF(p_interfaces[i].utf8().get_data()); + env->SetObjectArrayElement(j_interfaces, i, j_interface); + env->DeleteLocalRef(j_interface); + } + + jobject proxy = env->CallStaticObjectMethod(android_runtime_class, ARP_create_proxy_from_godot_object_id, object_id, j_interfaces); + Ref result = _jobject_to_variant(env, proxy); + + env->DeleteLocalRef(j_interfaces); + env->DeleteLocalRef(proxy); + + return result; +} + Ref JavaClassWrapper::wrap_jclass(jclass p_class, bool p_allow_non_public_methods_access) { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL_V(env, Ref()); @@ -1703,6 +1817,14 @@ Ref JavaClassWrapper::wrap_jclass(jclass p_class, bool p_allow_non_pu return _wrap(class_name_string, p_allow_non_public_methods_access); } +bool JavaClassWrapper::_is_proxy_class(JNIEnv *env, jclass p_class) { + ERR_FAIL_NULL_V(proxy_class, false); + ERR_FAIL_NULL_V(Proxy_isProxyClass, false); + ERR_FAIL_NULL_V(env, false); + + return env->CallStaticBooleanMethod(proxy_class, Proxy_isProxyClass, p_class); +} + JavaClassWrapper *JavaClassWrapper::singleton = nullptr; JavaClassWrapper::JavaClassWrapper() { @@ -1711,12 +1833,27 @@ JavaClassWrapper::JavaClassWrapper() { JNIEnv *env = get_jni_env(); ERR_FAIL_NULL(env); + proxy_class = jni_find_class(env, "java/lang/reflect/Proxy"); + if (proxy_class) { + proxy_class = (jclass)env->NewGlobalRef(proxy_class); + Proxy_isProxyClass = env->GetStaticMethodID(proxy_class, "isProxyClass", "(Ljava/lang/Class;)Z"); + } + + android_runtime_class = jni_find_class(env, "org/godotengine/godot/plugin/AndroidRuntimePlugin"); + if (android_runtime_class) { + android_runtime_class = (jclass)env->NewGlobalRef(android_runtime_class); + ARP_create_proxy_from_godot_callable = env->GetStaticMethodID(android_runtime_class, "createProxyFromGodotCallable", "(Ljava/lang/String;Lorg/godotengine/godot/variant/Callable;)Ljava/lang/Object;"); + ARP_create_proxy_from_godot_object_id = env->GetStaticMethodID(android_runtime_class, "createProxyFromGodotObjectID", "(J[Ljava/lang/String;)Ljava/lang/Object;"); + } + jclass bclass = jni_find_class(env, "java/lang/Class"); Class_getConstructors = env->GetMethodID(bclass, "getConstructors", "()[Ljava/lang/reflect/Constructor;"); Class_getDeclaredMethods = env->GetMethodID(bclass, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;"); Class_getFields = env->GetMethodID(bclass, "getFields", "()[Ljava/lang/reflect/Field;"); + Class_getInterfaces = env->GetMethodID(bclass, "getInterfaces", "()[Ljava/lang/Class;"); Class_getName = env->GetMethodID(bclass, "getName", "()Ljava/lang/String;"); Class_getSuperclass = env->GetMethodID(bclass, "getSuperclass", "()Ljava/lang/Class;"); + Class_isInterface = env->GetMethodID(bclass, "isInterface", "()Z"); env->DeleteLocalRef(bclass); bclass = jni_find_class(env, "java/lang/reflect/Constructor");