Parcourir la source

Godot Android plugin re-architecture

Fredia Huya-Kouadio il y a 2 ans
Parent
commit
8cc7739197

+ 2 - 0
core/extension/gdextension_manager.cpp

@@ -143,6 +143,8 @@ void GDExtensionManager::load_extensions() {
 			ERR_CONTINUE_MSG(err == LOAD_STATUS_FAILED, "Error loading extension: " + s);
 		}
 	}
+
+	OS::get_singleton()->load_platform_gdextensions();
 }
 
 GDExtensionManager *GDExtensionManager::get_singleton() {

+ 4 - 0
core/os/os.h

@@ -328,6 +328,10 @@ public:
 
 	virtual PreferredTextureFormat get_preferred_texture_format() const;
 
+	// Load GDExtensions specific to this platform.
+	// This is invoked by the GDExtensionManager after loading GDExtensions specified by the project.
+	virtual void load_platform_gdextensions() const {}
+
 	OS();
 	virtual ~OS();
 };

+ 9 - 0
editor/plugins/gdextension_export_plugin.h

@@ -50,6 +50,15 @@ void GDExtensionExportPlugin::_export_file(const String &p_path, const String &p
 	Error err = config->load(p_path);
 	ERR_FAIL_COND_MSG(err, "Failed to load GDExtension file: " + p_path);
 
+	// Check whether this GDExtension should be exported.
+	bool android_aar_plugin = config->get_value("configuration", "android_aar_plugin", false);
+	if (android_aar_plugin && p_features.has("android")) {
+		// The gdextension configuration and Android .so files will be provided by the Android aar
+		// plugin it's part of, so we abort here.
+		skip();
+		return;
+	}
+
 	ERR_FAIL_COND_MSG(!config->has_section_key("configuration", "entry_symbol"), "Failed to export GDExtension file, missing entry symbol: " + p_path);
 
 	String entry_symbol = config->get_value("configuration", "entry_symbol");

+ 13 - 0
platform/android/java/lib/src/org/godotengine/godot/Godot.kt

@@ -907,6 +907,19 @@ class Godot(private val context: Context) : SensorEventListener {
 		return PermissionsUtil.getGrantedPermissions(getActivity())
 	}
 
+	/**
+	 * Get the list of gdextension modules to register.
+	 */
+	@Keep
+	private fun getGDExtensionConfigFiles(): Array<String> {
+		val configFiles = mutableSetOf<String>()
+		for (plugin in pluginRegistry.allPlugins) {
+			configFiles.addAll(plugin.pluginGDExtensionLibrariesPaths)
+		}
+
+		return configFiles.toTypedArray()
+	}
+
 	@Keep
 	private fun getCACertificates(): String {
 		return GodotNetUtils.getCACertificates()

+ 50 - 91
platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java

@@ -57,27 +57,25 @@ import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
 
 /**
- * Base class for the Godot Android plugins.
+ * Base class for Godot Android plugins.
  * <p>
- * A Godot Android plugin is a regular Android library packaged as an aar archive file with the following caveats:
+ * A Godot Android plugin is an Android library with the following requirements:
  * <p>
- * - The library must have a dependency on the Godot Android library (godot-lib.aar).
- * A stable version is available for each release.
+ * - The library must have a 'compileOnly' dependency on the Godot Android library: `compileOnly "org.godotengine:godot:<godotLibVersion>"`
  * <p>
- * - The library must include a <meta-data> tag in its manifest file setup as follow:
- * <meta-data android:name="org.godotengine.plugin.v1.[PluginName]" android:value="[plugin.init.ClassFullName]" />
+ * - The library must include a <meta-data> tag in its Android manifest with the following format:
+ * <meta-data android:name="org.godotengine.plugin.v2.[PluginName]" android:value="[plugin.init.ClassFullName]" />
  * Where:
  * - 'PluginName' is the name of the plugin.
- * - 'plugin.init.ClassFullName' is the full name (package + class name) of the plugin class
+ * - 'plugin.init.ClassFullName' is the full name (package + class name) of the plugin init class
  * extending {@link GodotPlugin}.
+ * <p>
+ * A Godot Android plugin can also define and provide c/c++ gdextension libraries, which will be
+ * automatically bundled by the aar build system.
+ * GDExtension ('*.gdextension') config files must be located in the project 'assets' directory and
+ * their paths specified by {@link GodotPlugin#getPluginGDExtensionLibrariesPaths()}.
  *
- * A plugin can also define and provide c/c++ gdextension libraries and nativescripts for the target
- * app/game to leverage.
- * The shared library for the gdextension library will be automatically bundled by the aar build
- * system.
- * Godot '*.gdextension' resource files must however be manually defined in the project
- * 'assets' directory. The recommended path for these resources in the 'assets' directory should be:
- * 'godot/plugin/v1/[PluginName]/'
+ * @see <a href="https://docs.godotengine.org/en/stable/tutorials/platform/android/index.html">Android plugins</a>
  */
 public abstract class GodotPlugin {
 	private static final String TAG = GodotPlugin.class.getSimpleName();
@@ -97,7 +95,7 @@ public abstract class GodotPlugin {
 	}
 
 	/**
-	 * Provides access to the underlying {@link Activity}.
+	 * Provides access to the hosting {@link Activity}.
 	 */
 	@Nullable
 	protected Activity getActivity() {
@@ -106,33 +104,16 @@ public abstract class GodotPlugin {
 
 	/**
 	 * Register the plugin with Godot native code.
-	 *
-	 * This method is invoked on the render thread.
+	 * <p>
+	 * This method is invoked by the Godot Engine on the render thread.
 	 */
 	public final void onRegisterPluginWithGodotNative() {
 		registeredSignals.putAll(
-				registerPluginWithGodotNative(this, getPluginName(), getPluginMethods(), getPluginSignals(),
-						getPluginGDExtensionLibrariesPaths()));
-	}
-
-	/**
-	 * Register the plugin with Godot native code.
-	 *
-	 * This method must be invoked on the render thread.
-	 */
-	public static void registerPluginWithGodotNative(Object pluginObject,
-			GodotPluginInfoProvider pluginInfoProvider) {
-		registerPluginWithGodotNative(pluginObject, pluginInfoProvider.getPluginName(),
-				Collections.emptyList(), pluginInfoProvider.getPluginSignals(),
-				pluginInfoProvider.getPluginGDExtensionLibrariesPaths());
-
-		// Notify that registration is complete.
-		pluginInfoProvider.onPluginRegistered();
+				registerPluginWithGodotNative(this, getPluginName(), getPluginSignals()));
 	}
 
 	private static Map<String, SignalInfo> registerPluginWithGodotNative(Object pluginObject,
-			String pluginName, List<String> pluginMethods, Set<SignalInfo> pluginSignals,
-			Set<String> pluginGDExtensionLibrariesPaths) {
+			String pluginName, Set<SignalInfo> pluginSignals) {
 		nativeRegisterSingleton(pluginName, pluginObject);
 
 		Set<Method> filteredMethods = new HashSet<>();
@@ -143,14 +124,6 @@ public abstract class GodotPlugin {
 			// 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;
-					}
-				}
 			}
 		}
 
@@ -176,23 +149,18 @@ public abstract class GodotPlugin {
 			registeredSignals.put(signalName, signalInfo);
 		}
 
-		// Get the list of gdextension libraries to register.
-		if (!pluginGDExtensionLibrariesPaths.isEmpty()) {
-			nativeRegisterGDExtensionLibraries(pluginGDExtensionLibrariesPaths.toArray(new String[0]));
-		}
-
 		return registeredSignals;
 	}
 
 	/**
-	 * Invoked once during the Godot Android initialization process after creation of the
+	 * Invoked once during the initialization process after creation of the
 	 * {@link org.godotengine.godot.GodotRenderView} view.
 	 * <p>
-	 * The plugin can return a non-null {@link View} layout in order to add it to the Godot view
+	 * The plugin can return a non-null {@link View} layout which will be added to the Godot view
 	 * hierarchy.
-	 *
-	 * Use shouldBeOnTop() to set whether the plugin's {@link View} should be added on top or behind
-	 * the main Godot view.
+	 * <p>
+	 * Use {@link GodotPlugin#shouldBeOnTop()} to specify whether the plugin's {@link View} should
+	 * be added on top or behind the main Godot view.
 	 *
 	 * @see Activity#onCreate(Bundle)
 	 * @return the plugin's view to be included; null if no views should be included.
@@ -235,44 +203,52 @@ public abstract class GodotPlugin {
 	public boolean onMainBackPressed() { return false; }
 
 	/**
-	 * Invoked on the render thread when the Godot setup is complete.
+	 * Invoked on the render thread when set up of the Godot engine is complete.
+	 * <p>
+	 * This is invoked before {@link GodotPlugin#onGodotMainLoopStarted()}.
 	 */
 	public void onGodotSetupCompleted() {}
 
 	/**
 	 * Invoked on the render thread when the Godot main loop has started.
+	 *
+	 * This is invoked after {@link GodotPlugin#onGodotSetupCompleted()}.
 	 */
 	public void onGodotMainLoopStarted() {}
 
 	/**
-	 * Invoked once per frame on the GL thread after the frame is drawn.
+	 * When using the OpenGL renderer, this is invoked once per frame on the GL thread after the
+	 * frame is drawn.
 	 */
 	public void onGLDrawFrame(GL10 gl) {}
 
 	/**
-	 * Called on the GL thread after the surface is created and whenever the OpenGL ES surface size
-	 * changes.
+	 * When using the OpenGL renderer, this is called on the GL thread after the surface is created
+	 * and whenever the OpenGL ES surface size changes.
 	 */
 	public void onGLSurfaceChanged(GL10 gl, int width, int height) {}
 
 	/**
-	 * Called on the GL thread when the surface is created or recreated.
+	 * When using the OpenGL renderer, this is called on the GL thread when the surface is created
+	 * or recreated.
 	 */
 	public void onGLSurfaceCreated(GL10 gl, EGLConfig config) {}
 
 	/**
-	 * Invoked once per frame on the Vulkan thread after the frame is drawn.
+	 * When using the Vulkan renderer, this is invoked once per frame on the Vulkan thread after
+	 * the frame is drawn.
 	 */
 	public void onVkDrawFrame() {}
 
 	/**
-	 * Called on the Vulkan thread after the surface is created and whenever the surface size
-	 * changes.
+	 * When using the Vulkan renderer, this is called on the Vulkan thread after the surface is
+	 * created and whenever the surface size changes.
 	 */
 	public void onVkSurfaceChanged(Surface surface, int width, int height) {}
 
 	/**
-	 * Called on the Vulkan thread when the surface is created or recreated.
+	 * When using the Vulkan renderer, this is called on the Vulkan thread when the surface is
+	 * created or recreated.
 	 */
 	public void onVkSurfaceCreated(Surface surface) {}
 
@@ -284,17 +260,6 @@ public abstract class GodotPlugin {
 	@NonNull
 	public abstract String getPluginName();
 
-	/**
-	 * 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();
-	}
-
 	/**
 	 * Returns the list of signals to be exposed to Godot.
 	 */
@@ -304,19 +269,19 @@ public abstract class GodotPlugin {
 	}
 
 	/**
-	 * Returns the paths for the plugin's gdextension libraries.
-	 *
-	 * The paths must be relative to the 'assets' directory and point to a '*.gdextension' file.
+	 * Returns the paths for the plugin's gdextension libraries (if any).
+	 * <p>
+	 * Each returned path must be relative to the 'assets' directory and point to a '*.gdextension' file.
 	 */
 	@NonNull
-	protected Set<String> getPluginGDExtensionLibrariesPaths() {
+	public Set<String> getPluginGDExtensionLibrariesPaths() {
 		return Collections.emptySet();
 	}
 
 	/**
-	 * Returns whether the plugin's {@link View} returned in onMainCreate() should be placed on
-	 * top of the main Godot view.
-	 *
+	 * Returns whether the plugin's {@link View} returned in
+	 * {@link GodotPlugin#onMainCreate(Activity)} should be placed on top of the main Godot view.
+	 * <p>
 	 * Returning false causes the plugin's {@link View} to be placed behind, which can be useful
 	 * when used with transparency in order to let the Godot view handle inputs.
 	 */
@@ -359,7 +324,7 @@ public abstract class GodotPlugin {
 			}
 			emitSignal(getGodot(), getPluginName(), signalInfo, signalArgs);
 		} catch (IllegalArgumentException exception) {
-			Log.w(TAG, exception.getMessage());
+			Log.w(TAG, exception);
 			if (BuildConfig.DEBUG) {
 				throw exception;
 			}
@@ -368,7 +333,7 @@ public abstract class GodotPlugin {
 
 	/**
 	 * Emit a Godot signal.
-	 * @param godot
+	 * @param godot Godot instance
 	 * @param pluginName Name of the Godot plugin the signal will be emitted from. The plugin must already be registered with the Godot engine.
 	 * @param signalInfo Information about the signal to emit.
 	 * @param signalArgs Arguments used to populate the emitted signal. The arguments will be validated against the given {@link SignalInfo} parameter.
@@ -397,7 +362,7 @@ public abstract class GodotPlugin {
 			godot.runOnRenderThread(() -> nativeEmitSignal(pluginName, signalInfo.getName(), signalArgs));
 
 		} catch (IllegalArgumentException exception) {
-			Log.w(TAG, exception.getMessage());
+			Log.w(TAG, exception);
 			if (BuildConfig.DEBUG) {
 				throw exception;
 			}
@@ -420,13 +385,7 @@ public abstract class GodotPlugin {
 	private static native void nativeRegisterMethod(String p_sname, String p_name, String p_ret, String[] p_params);
 
 	/**
-	 * Used to register gdextension libraries bundled by the plugin.
-	 * @param gdextensionPaths Paths to the libraries relative to the 'assets' directory.
-	 */
-	private static native void nativeRegisterGDExtensionLibraries(String[] gdextensionPaths);
-
-	/**
-	 * Used to complete registration of the {@link GodotPlugin} instance's methods.
+	 * Used to complete registration of the {@link GodotPlugin} instance's signals.
 	 * @param pluginName Name of the plugin
 	 * @param signalName Name of the signal to register
 	 * @param signalParamTypes Signal parameters types

+ 0 - 72
platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginInfoProvider.java

@@ -1,72 +0,0 @@
-/**************************************************************************/
-/*  GodotPluginInfoProvider.java                                          */
-/**************************************************************************/
-/*                         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.plugin;
-
-import androidx.annotation.NonNull;
-
-import java.util.Collections;
-import java.util.Set;
-
-/**
- * Provides the set of information expected from a Godot plugin.
- */
-public interface GodotPluginInfoProvider {
-	/**
-	 * Returns the name of the plugin.
-	 */
-	@NonNull
-	String getPluginName();
-
-	/**
-	 * Returns the list of signals to be exposed to Godot.
-	 */
-	@NonNull
-	default Set<SignalInfo> getPluginSignals() {
-		return Collections.emptySet();
-	}
-
-	/**
-	 * Returns the paths for the plugin's gdextension libraries (if any).
-	 *
-	 * The paths must be relative to the 'assets' directory and point to a '*.gdextension' file.
-	 */
-	@NonNull
-	default Set<String> getPluginGDExtensionLibrariesPaths() {
-		return Collections.emptySet();
-	}
-
-	/**
-	 * This is invoked on the render thread when the plugin described by this instance has been
-	 * registered.
-	 */
-	default void onPluginRegistered() {
-	}
-}

+ 20 - 16
platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java

@@ -52,7 +52,14 @@ import java.util.concurrent.ConcurrentHashMap;
 public final class GodotPluginRegistry {
 	private static final String TAG = GodotPluginRegistry.class.getSimpleName();
 
+	/**
+	 * Prefix used for version 1 of the Godot plugin, compatible with Godot 3.x
+	 */
 	private static final String GODOT_PLUGIN_V1_NAME_PREFIX = "org.godotengine.plugin.v1.";
+	/**
+	 * Prefix used for version 2 of the Godot plugin, compatible with Godot 4.x
+	 */
+	private static final String GODOT_PLUGIN_V2_NAME_PREFIX = "org.godotengine.plugin.v2.";
 
 	private static GodotPluginRegistry instance;
 	private final ConcurrentHashMap<String, GodotPlugin> registry;
@@ -123,12 +130,12 @@ public final class GodotPluginRegistry {
 				return;
 			}
 
-			int godotPluginV1NamePrefixLength = GODOT_PLUGIN_V1_NAME_PREFIX.length();
+			int godotPluginV2NamePrefixLength = GODOT_PLUGIN_V2_NAME_PREFIX.length();
 			for (String metaDataName : metaData.keySet()) {
 				// Parse the meta-data looking for entry with the Godot plugin name prefix.
-				if (metaDataName.startsWith(GODOT_PLUGIN_V1_NAME_PREFIX)) {
-					String pluginName = metaDataName.substring(godotPluginV1NamePrefixLength).trim();
-					Log.i(TAG, "Initializing Godot plugin " + pluginName);
+				if (metaDataName.startsWith(GODOT_PLUGIN_V2_NAME_PREFIX)) {
+					String pluginName = metaDataName.substring(godotPluginV2NamePrefixLength).trim();
+					Log.i(TAG, "Initializing Godot v2 plugin " + pluginName);
 
 					// Retrieve the plugin class full name.
 					String pluginHandleClassFullName = metaData.getString(metaDataName);
@@ -148,25 +155,22 @@ public final class GodotPluginRegistry {
 										"Meta-data plugin name does not match the value returned by the plugin handle: " + pluginName + " =/= " + pluginHandle.getPluginName());
 							}
 							registry.put(pluginName, pluginHandle);
-							Log.i(TAG, "Completed initialization for Godot plugin " + pluginHandle.getPluginName());
-						} catch (ClassNotFoundException e) {
-							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
-						} catch (IllegalAccessException e) {
-							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
-						} catch (InstantiationException e) {
-							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
-						} catch (NoSuchMethodException e) {
-							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
-						} catch (InvocationTargetException e) {
-							Log.w(TAG, "Unable to load Godot plugin " + pluginName, e);
+							Log.i(TAG, "Completed initialization for Godot v2 plugin " + pluginHandle.getPluginName());
+						} catch (ClassNotFoundException | IllegalAccessException |
+								InstantiationException | NoSuchMethodException |
+								InvocationTargetException e) {
+							Log.w(TAG, "Unable to load Godot v2 plugin " + pluginName, e);
 						}
 					} else {
 						Log.w(TAG, "Invalid plugin loader class for " + pluginName);
 					}
+				} else if (metaDataName.startsWith(GODOT_PLUGIN_V1_NAME_PREFIX)) {
+					String v1PluginName = metaDataName.substring(GODOT_PLUGIN_V1_NAME_PREFIX.length()).trim();
+					Log.w(TAG, "Godot 4 does not support Godot 3 (v1) plugin: " + v1PluginName);
 				}
 			}
 		} catch (PackageManager.NameNotFoundException e) {
-			Log.e(TAG, "Unable load Godot Android plugins from the manifest file.", e);
+			Log.e(TAG, "Unable load Godot Android v2 plugins from the manifest file.", e);
 		}
 	}
 }

+ 20 - 0
platform/android/java_godot_wrapper.cpp

@@ -79,6 +79,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 	_begin_benchmark_measure = p_env->GetMethodID(godot_class, "nativeBeginBenchmarkMeasure", "(Ljava/lang/String;)V");
 	_end_benchmark_measure = p_env->GetMethodID(godot_class, "nativeEndBenchmarkMeasure", "(Ljava/lang/String;)V");
 	_dump_benchmark = p_env->GetMethodID(godot_class, "nativeDumpBenchmark", "(Ljava/lang/String;)V");
+	_get_gdextension_list_config_file = p_env->GetMethodID(godot_class, "getGDExtensionConfigFiles", "()[Ljava/lang/String;");
 }
 
 GodotJavaWrapper::~GodotJavaWrapper() {
@@ -264,6 +265,25 @@ Vector<String> GodotJavaWrapper::get_granted_permissions() const {
 	return permissions_list;
 }
 
+Vector<String> GodotJavaWrapper::get_gdextension_list_config_file() const {
+	Vector<String> config_file_list;
+	if (_get_gdextension_list_config_file) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_NULL_V(env, config_file_list);
+		jobject config_file_list_object = env->CallObjectMethod(godot_instance, _get_gdextension_list_config_file);
+		jobjectArray *arr = reinterpret_cast<jobjectArray *>(&config_file_list_object);
+
+		jsize len = env->GetArrayLength(*arr);
+		for (int i = 0; i < len; i++) {
+			jstring j_config_file = (jstring)env->GetObjectArrayElement(*arr, i);
+			String config_file = jstring_to_string(j_config_file, env);
+			config_file_list.push_back(config_file);
+			env->DeleteLocalRef(j_config_file);
+		}
+	}
+	return config_file_list;
+}
+
 String GodotJavaWrapper::get_ca_certificates() const {
 	if (_get_ca_certificates) {
 		JNIEnv *env = get_jni_env();

+ 4 - 0
platform/android/java_godot_wrapper.h

@@ -59,6 +59,7 @@ private:
 	jmethodID _request_permission = nullptr;
 	jmethodID _request_permissions = nullptr;
 	jmethodID _get_granted_permissions = nullptr;
+	jmethodID _get_gdextension_list_config_file = nullptr;
 	jmethodID _get_ca_certificates = nullptr;
 	jmethodID _init_input_devices = nullptr;
 	jmethodID _vibrate = nullptr;
@@ -102,6 +103,9 @@ public:
 	void begin_benchmark_measure(const String &p_label);
 	void end_benchmark_measure(const String &p_label);
 	void dump_benchmark(const String &benchmark_file);
+
+	// Return the list of gdextensions config file.
+	Vector<String> get_gdextension_list_config_file() const;
 };
 
 #endif // JAVA_GODOT_WRAPPER_H

+ 41 - 0
platform/android/os_android.cpp

@@ -39,6 +39,7 @@
 #include "net_socket_android.h"
 
 #include "core/config/project_settings.h"
+#include "core/extension/gdextension_manager.h"
 #include "drivers/unix/dir_access_unix.h"
 #include "drivers/unix/file_access_unix.h"
 #include "main/main.h"
@@ -162,11 +163,39 @@ Vector<String> OS_Android::get_granted_permissions() const {
 
 Error OS_Android::open_dynamic_library(const String p_path, void *&p_library_handle, bool p_also_set_library_path, String *r_resolved_path) {
 	String path = p_path;
+	bool so_file_exists = true;
 	if (!FileAccess::exists(path)) {
 		path = p_path.get_file();
+		so_file_exists = false;
 	}
 
 	p_library_handle = dlopen(path.utf8().get_data(), RTLD_NOW);
+	if (!p_library_handle && so_file_exists) {
+		// The library may be on the sdcard and thus inaccessible. Try to copy it to the internal
+		// directory.
+		uint64_t so_modified_time = FileAccess::get_modified_time(p_path);
+		String dynamic_library_path = get_dynamic_libraries_path().path_join(String::num_uint64(so_modified_time));
+		String internal_path = dynamic_library_path.path_join(p_path.get_file());
+
+		bool internal_so_file_exists = FileAccess::exists(internal_path);
+		if (!internal_so_file_exists) {
+			Ref<DirAccess> da_ref = DirAccess::create_for_path(p_path);
+			if (da_ref.is_valid()) {
+				Error create_dir_result = da_ref->make_dir_recursive(dynamic_library_path);
+				if (create_dir_result == OK || create_dir_result == ERR_ALREADY_EXISTS) {
+					internal_so_file_exists = da_ref->copy(path, internal_path) == OK;
+				}
+			}
+		}
+
+		if (internal_so_file_exists) {
+			p_library_handle = dlopen(internal_path.utf8().get_data(), RTLD_NOW);
+			if (p_library_handle) {
+				path = internal_path;
+			}
+		}
+	}
+
 	ERR_FAIL_NULL_V_MSG(p_library_handle, ERR_CANT_OPEN, vformat("Can't open dynamic library: %s. Error: %s.", p_path, dlerror()));
 
 	if (r_resolved_path != nullptr) {
@@ -584,6 +613,10 @@ String OS_Android::get_user_data_dir() const {
 	return ".";
 }
 
+String OS_Android::get_dynamic_libraries_path() const {
+	return get_cache_path().path_join("dynamic_libraries");
+}
+
 String OS_Android::get_cache_path() const {
 	if (!cache_dir_cache.is_empty()) {
 		return cache_dir_cache;
@@ -791,5 +824,13 @@ Error OS_Android::setup_remote_filesystem(const String &p_server_host, int p_por
 	return err;
 }
 
+void OS_Android::load_platform_gdextensions() const {
+	Vector<String> extension_list_config_file = godot_java->get_gdextension_list_config_file();
+	for (String config_file_path : extension_list_config_file) {
+		GDExtensionManager::LoadStatus err = GDExtensionManager::get_singleton()->load_extension(config_file_path);
+		ERR_CONTINUE_MSG(err == GDExtensionManager::LOAD_STATUS_FAILED, "Error loading platform extension: " + config_file_path);
+	}
+}
+
 OS_Android::~OS_Android() {
 }

+ 6 - 0
platform/android/os_android.h

@@ -169,9 +169,15 @@ public:
 	virtual void benchmark_end_measure(const String &p_what) override;
 	virtual void benchmark_dump() override;
 
+	virtual void load_platform_gdextensions() const override;
+
 	virtual bool _check_internal_feature_support(const String &p_feature) override;
 	OS_Android(GodotJavaWrapper *p_godot_java, GodotIOJavaWrapper *p_godot_io_java, bool p_use_apk_expansion);
 	~OS_Android();
+
+private:
+	// Location where we relocate external dynamic libraries to make them accessible.
+	String get_dynamic_libraries_path() const;
 };
 
 #endif // OS_ANDROID_H

+ 0 - 27
platform/android/plugin/godot_plugin_jni.cpp

@@ -129,31 +129,4 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitS
 
 	singleton->emit_signalp(StringName(signal_name), args, count);
 }
-
-JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDExtensionLibraries(JNIEnv *env, jclass clazz, jobjectArray gdextension_paths) {
-	int gdextension_count = env->GetArrayLength(gdextension_paths);
-	if (gdextension_count == 0) {
-		return;
-	}
-
-	// Retrieve the current list of gdextension libraries.
-	Array singletons;
-	if (ProjectSettings::get_singleton()->has_setting("gdextension/singletons")) {
-		singletons = GLOBAL_GET("gdextension/singletons");
-	}
-
-	// Insert the libraries provided by the plugin
-	for (int i = 0; i < gdextension_count; i++) {
-		jstring relative_path = (jstring)env->GetObjectArrayElement(gdextension_paths, i);
-
-		String path = "res://" + jstring_to_string(relative_path, env);
-		if (!singletons.has(path)) {
-			singletons.push_back(path);
-		}
-		env->DeleteLocalRef(relative_path);
-	}
-
-	// Insert the updated list back into project settings.
-	ProjectSettings::get_singleton()->set("gdextension/singletons", singletons);
-}
 }

+ 0 - 1
platform/android/plugin/godot_plugin_jni.h

@@ -39,7 +39,6 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegis
 JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterMethod(JNIEnv *env, jclass clazz, jstring sname, jstring name, jstring ret, jobjectArray args);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterSignal(JNIEnv *env, jclass clazz, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_param_types);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeEmitSignal(JNIEnv *env, jclass clazz, jstring j_plugin_name, jstring j_signal_name, jobjectArray j_signal_params);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_plugin_GodotPlugin_nativeRegisterGDExtensionLibraries(JNIEnv *env, jclass clazz, jobjectArray gdextension_paths);
 }
 
 #endif // GODOT_PLUGIN_JNI_H